mirror of
https://github.com/suitenumerique/meet.git
synced 2026-09-30 22:48:35 +00:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 22adccb353 | |||
| 3ab651d6c7 | |||
| 919af928aa | |||
| 3ed38f1c48 | |||
| f172c5795e | |||
| 262b168414 | |||
| 6c371c8cb3 |
@@ -0,0 +1,9 @@
|
||||
[codespell]
|
||||
# Files that are not English, or generated
|
||||
skip = ./.git,*.pdf,*.po,*.pot,*.json,*.lock,package-lock.json,
|
||||
./LICENSES,
|
||||
./src/summary/summary/core/locales,
|
||||
./src/summary/summary/core/prompt.py
|
||||
# Valid words in French (connexion) or in the code (statics)
|
||||
ignore-words-list = connexion,statics
|
||||
check-filenames = true
|
||||
@@ -0,0 +1,20 @@
|
||||
# Debian 13 base image (python:3.14-slim): no fixed version available yet.
|
||||
# Review regularly and remove entries once Debian ships a fix.
|
||||
|
||||
# util-linux
|
||||
CVE-2026-76642
|
||||
CVE-2026-78408
|
||||
CVE-2026-78409
|
||||
CVE-2026-78410
|
||||
|
||||
# acl
|
||||
CVE-2026-54369
|
||||
|
||||
# ncurses
|
||||
CVE-2025-69720
|
||||
|
||||
# systemd
|
||||
CVE-2026-16742
|
||||
|
||||
# perl-base (fix deferred by Debian)
|
||||
CVE-2026-9538
|
||||
@@ -0,0 +1,19 @@
|
||||
name: Changelog Workflow
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, labeled, unlabeled]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event.pull_request.number || github.sha }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
changelog:
|
||||
uses: suitenumerique/ci/.github/workflows/_changelog.yml@ca6401ac83e981e5b0a94f06f68baaa6e6d0a436 # v0.0.1
|
||||
+22
-176
@@ -11,180 +11,30 @@ permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
lint-git:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'pull_request' # Makes sense only for pull requests
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: show
|
||||
run: git log
|
||||
- name: Enforce absence of print statements in code
|
||||
if: always()
|
||||
run: |
|
||||
! git diff origin/${{ github.event.pull_request.base.ref }}..HEAD -- . ':(exclude).github/workflows/**' | grep "print("
|
||||
- name: Check absence of fixup commits
|
||||
if: always()
|
||||
run: |
|
||||
! git log | grep 'fixup!'
|
||||
- name: Install uv
|
||||
if: always()
|
||||
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
|
||||
- name: Lint commit messages added to main
|
||||
if: always()
|
||||
run: uvx --no-build --from gitlint-core==0.19.1 gitlint --commits origin/${{ github.event.pull_request.base.ref }}..HEAD
|
||||
|
||||
check-changelog:
|
||||
runs-on: ubuntu-latest
|
||||
if: |
|
||||
contains(github.event.pull_request.labels.*.name, 'noChangeLog') == false &&
|
||||
github.event_name == 'pull_request'
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
|
||||
with:
|
||||
fetch-depth: 50
|
||||
- name: Check that the CHANGELOG has been modified in the current branch
|
||||
run: git diff --name-only ${{ github.event.pull_request.base.sha }} ${{ github.event.after }} | grep 'CHANGELOG.md'
|
||||
|
||||
lint-changelog:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
|
||||
- name: Check CHANGELOG max line length
|
||||
run: |
|
||||
max_line_length=$(cat CHANGELOG.md | grep -Ev "^\[.*\]: https://github.com" | wc -L)
|
||||
if [ $max_line_length -ge 80 ]; then
|
||||
echo "ERROR: CHANGELOG has lines longer than 80 characters."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
build-mails:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
defaults:
|
||||
run:
|
||||
working-directory: src/mail
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
|
||||
with:
|
||||
node-version: "22"
|
||||
|
||||
- name: Restore the mail templates
|
||||
uses: actions/cache@caa296126883cff596d87d8935842f9db880ef25 # v5.1.0
|
||||
id: mail-templates
|
||||
with:
|
||||
path: "src/backend/core/templates/mail"
|
||||
key: mail-templates-${{ hashFiles('src/mail/mjml') }}
|
||||
|
||||
- name: Install yarn
|
||||
if: steps.mail-templates.outputs.cache-hit != 'true'
|
||||
run: npm install -g --ignore-scripts yarn@1.22.22
|
||||
|
||||
- name: Install node dependencies
|
||||
if: steps.mail-templates.outputs.cache-hit != 'true'
|
||||
run: yarn install --frozen-lockfile --ignore-scripts
|
||||
|
||||
- name: Build mails
|
||||
if: steps.mail-templates.outputs.cache-hit != 'true'
|
||||
run: yarn build
|
||||
|
||||
- name: Cache mail templates
|
||||
if: steps.mail-templates.outputs.cache-hit != 'true'
|
||||
uses: actions/cache@caa296126883cff596d87d8935842f9db880ef25 # v5.1.0
|
||||
with:
|
||||
path: "src/backend/core/templates/mail"
|
||||
key: mail-templates-${{ hashFiles('src/mail/mjml') }}
|
||||
|
||||
lint-back:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
defaults:
|
||||
run:
|
||||
working-directory: src/backend
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
|
||||
- name: Install Python
|
||||
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
||||
with:
|
||||
python-version: "3.13"
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
|
||||
- name: Install the project
|
||||
run: uv sync --locked --all-extras
|
||||
|
||||
- name: Check code formatting with ruff
|
||||
run: uv run --no-sync --no-build ruff format . --diff
|
||||
- name: Lint code with ruff
|
||||
run: uv run --no-sync --no-build ruff check .
|
||||
- name: Lint code with pylint
|
||||
run: uv run --no-sync --no-build pylint meet demo core
|
||||
|
||||
lint-agents:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
defaults:
|
||||
run:
|
||||
working-directory: src/agents
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
|
||||
- name: Install Python
|
||||
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
||||
with:
|
||||
python-version: "3.13"
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
|
||||
- name: Install the project
|
||||
run: uv sync --locked --all-extras --no-build
|
||||
- name: Check code formatting with ruff
|
||||
run: uv run --no-sync --no-build ruff format . --diff
|
||||
- name: Lint code with ruff
|
||||
run: uv run --no-sync --no-build ruff check .
|
||||
|
||||
lint-summary:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
defaults:
|
||||
run:
|
||||
working-directory: src/summary
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
|
||||
- name: Install Python
|
||||
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
||||
with:
|
||||
python-version: "3.13"
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
|
||||
- name: Install the project
|
||||
run: uv sync --locked --all-extras
|
||||
- name: Check code formatting with ruff
|
||||
run: uv run --no-sync --no-build ruff format . --diff
|
||||
- name: Lint code with ruff
|
||||
run: uv run --no-sync --no-build ruff check .
|
||||
lint-python:
|
||||
name: lint ${{ matrix.service }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- service: backend
|
||||
working_directory: src/backend
|
||||
pylint_targets: meet demo core
|
||||
- service: agents
|
||||
working_directory: src/agents
|
||||
pylint_targets: ""
|
||||
- service: summary
|
||||
working_directory: src/summary
|
||||
pylint_targets: ""
|
||||
uses: suitenumerique/ci/.github/workflows/_python-lint.yml@ca6401ac83e981e5b0a94f06f68baaa6e6d0a436 # v0.0.1
|
||||
with:
|
||||
working_directory: ${{ matrix.working_directory }}
|
||||
python_version: "3.13"
|
||||
pylint_targets: ${{ matrix.pylint_targets }}
|
||||
|
||||
test-back:
|
||||
runs-on: ubuntu-latest
|
||||
needs: build-mails
|
||||
permissions:
|
||||
contents: read
|
||||
defaults:
|
||||
@@ -244,12 +94,8 @@ jobs:
|
||||
sudo mkdir -p /data/media && \
|
||||
sudo mkdir -p /data/static
|
||||
|
||||
- name: Restore the mail templates
|
||||
uses: actions/cache@caa296126883cff596d87d8935842f9db880ef25 # v5.1.0
|
||||
id: mail-templates
|
||||
with:
|
||||
path: "src/backend/core/templates/mail"
|
||||
key: mail-templates-${{ hashFiles('src/mail/mjml') }}
|
||||
- name: Build or restore the mail templates
|
||||
uses: suitenumerique/ci/actions/mail-templates@ca6401ac83e981e5b0a94f06f68baaa6e6d0a436 # v0.0.1
|
||||
|
||||
# Creates the access key and the bucket on startup
|
||||
- name: Start Garage
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
name: Project quality Workflow
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
quality:
|
||||
uses: suitenumerique/ci/.github/workflows/_project-quality.yml@ca6401ac83e981e5b0a94f06f68baaa6e6d0a436 # v0.0.1
|
||||
with:
|
||||
print_check_paths: src/backend src/summary src/agents
|
||||
codespell_ignore_words: "unsecure"
|
||||
@@ -1,33 +0,0 @@
|
||||
name: Download Crowdin translations
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
types: [file-fully-translated]
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
crowdin:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
|
||||
|
||||
- name: Download Crowdin files
|
||||
uses: crowdin/github-action@c7af9bc98b01694653031fef2a0dc6c7888ce9bc # v2.17.0
|
||||
with:
|
||||
upload_sources: false
|
||||
upload_translations: false
|
||||
download_translations: true
|
||||
localization_branch_name: l10n_crowdin_translations
|
||||
create_pull_request: true
|
||||
pull_request_title: "New Crowdin translations"
|
||||
pull_request_body: "New Crowdin pull request with translations"
|
||||
pull_request_base_branch_name: "main"
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
|
||||
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
|
||||
CROWDIN_BASE_PATH: ${{ github.workspace }}
|
||||
@@ -1,5 +1,5 @@
|
||||
name: Docker Hub Workflow
|
||||
run-name: Docker Hub Workflow
|
||||
name: Docker images
|
||||
run-name: Docker images
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
@@ -15,265 +15,62 @@ on:
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
DOCKER_USER: 1001:127
|
||||
DOCKER_CONTAINER_REGISTRY_HOSTNAME: docker.io
|
||||
DOCKER_CONTAINER_REGISTRY_NAMESPACE: lasuite
|
||||
IS_MULTI_PLATFORM_BUILD: ${{ startsWith(github.ref, 'refs/tags/v') }}
|
||||
BUILD_PLATFORMS: ${{ startsWith(github.ref, 'refs/tags/v') && 'linux/amd64,linux/arm64' || 'linux/amd64' }}
|
||||
|
||||
jobs:
|
||||
build-and-push-backend:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
-
|
||||
name: Checkout repository
|
||||
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
|
||||
-
|
||||
name: Set up QEMU
|
||||
if: env.IS_MULTI_PLATFORM_BUILD == 'true'
|
||||
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
||||
-
|
||||
name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
|
||||
with:
|
||||
images: '${{ env.DOCKER_CONTAINER_REGISTRY_NAMESPACE }}/meet-backend'
|
||||
-
|
||||
name: Login to DockerHub
|
||||
if: github.event_name != 'pull_request' || startsWith(github.head_ref, 'integration/')
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USER }}
|
||||
password: ${{ secrets.DOCKER_HUB_PASSWORD }}
|
||||
-
|
||||
name: Run trivy scan
|
||||
uses: numerique-gouv/action-trivy-cache@d6e94cfb488f03a0b3e8b8739aad94e74d24d8da # main
|
||||
with:
|
||||
docker-build-args: '--target backend-production -f Dockerfile'
|
||||
docker-image-name: '${{ env.DOCKER_CONTAINER_REGISTRY_HOSTNAME }}/${{ env.DOCKER_CONTAINER_REGISTRY_NAMESPACE }}/meet-backend:${{ github.sha }}'
|
||||
-
|
||||
name: Build and push
|
||||
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
|
||||
with:
|
||||
context: .
|
||||
target: backend-production
|
||||
platforms: ${{ env.BUILD_PLATFORMS }}
|
||||
build-args: DOCKER_USER=${{ env.DOCKER_USER }}:-1000
|
||||
push: ${{ github.event_name != 'pull_request' || startsWith(github.head_ref, 'integration/') }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
build-and-push-frontend-generic:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
-
|
||||
name: Checkout repository
|
||||
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
|
||||
-
|
||||
name: Set up QEMU
|
||||
if: env.IS_MULTI_PLATFORM_BUILD == 'true'
|
||||
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
||||
-
|
||||
name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
|
||||
with:
|
||||
images: '${{ env.DOCKER_CONTAINER_REGISTRY_NAMESPACE }}/meet-frontend'
|
||||
-
|
||||
name: Login to DockerHub
|
||||
if: github.event_name != 'pull_request' || startsWith(github.head_ref, 'integration/')
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USER }}
|
||||
password: ${{ secrets.DOCKER_HUB_PASSWORD }}
|
||||
-
|
||||
name: Run trivy scan
|
||||
uses: numerique-gouv/action-trivy-cache@d6e94cfb488f03a0b3e8b8739aad94e74d24d8da # main
|
||||
with:
|
||||
docker-build-args: '-f src/frontend/Dockerfile --target frontend-production'
|
||||
docker-image-name: '${{ env.DOCKER_CONTAINER_REGISTRY_HOSTNAME }}/${{ env.DOCKER_CONTAINER_REGISTRY_NAMESPACE }}/meet-frontend:${{ github.sha }}'
|
||||
-
|
||||
name: Build and push
|
||||
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
|
||||
with:
|
||||
context: .
|
||||
file: ./src/frontend/Dockerfile
|
||||
target: frontend-production
|
||||
platforms: ${{ env.BUILD_PLATFORMS }}
|
||||
build-args: DOCKER_USER=${{ env.DOCKER_USER }}:-1000
|
||||
push: ${{ github.event_name != 'pull_request' || startsWith(github.head_ref, 'integration/') }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
build-and-push-frontend-dinum:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
-
|
||||
name: Checkout repository
|
||||
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
|
||||
-
|
||||
name: Set up QEMU
|
||||
if: env.IS_MULTI_PLATFORM_BUILD == 'true'
|
||||
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
||||
-
|
||||
name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
|
||||
with:
|
||||
images: '${{ env.DOCKER_CONTAINER_REGISTRY_NAMESPACE }}/meet-frontend-dinum'
|
||||
-
|
||||
name: Login to DockerHub
|
||||
if: github.event_name != 'pull_request' || startsWith(github.head_ref, 'integration/')
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USER }}
|
||||
password: ${{ secrets.DOCKER_HUB_PASSWORD }}
|
||||
-
|
||||
name: Run trivy scan
|
||||
uses: numerique-gouv/action-trivy-cache@d6e94cfb488f03a0b3e8b8739aad94e74d24d8da # main
|
||||
with:
|
||||
docker-build-args: '-f docker/dinum-frontend/Dockerfile --target frontend-production'
|
||||
docker-image-name: '${{ env.DOCKER_CONTAINER_REGISTRY_HOSTNAME }}/${{ env.DOCKER_CONTAINER_REGISTRY_NAMESPACE }}/meet-frontend-dinum:${{ github.sha }}'
|
||||
-
|
||||
name: Build and push
|
||||
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
|
||||
with:
|
||||
context: .
|
||||
file: ./docker/dinum-frontend/Dockerfile
|
||||
target: frontend-production
|
||||
platforms: ${{ env.BUILD_PLATFORMS }}
|
||||
build-args: DOCKER_USER=${{ env.DOCKER_USER }}:-1000
|
||||
push: ${{ github.event_name != 'pull_request' || startsWith(github.head_ref, 'integration/') }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
build-and-push-summary:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
-
|
||||
name: Checkout repository
|
||||
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
|
||||
-
|
||||
name: Set up QEMU
|
||||
if: env.IS_MULTI_PLATFORM_BUILD == 'true'
|
||||
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
||||
-
|
||||
name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
|
||||
with:
|
||||
images: '${{ env.DOCKER_CONTAINER_REGISTRY_NAMESPACE }}/meet-summary'
|
||||
-
|
||||
name: Login to DockerHub
|
||||
if: github.event_name != 'pull_request' || startsWith(github.head_ref, 'integration/')
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USER }}
|
||||
password: ${{ secrets.DOCKER_HUB_PASSWORD }}
|
||||
-
|
||||
name: Run trivy scan
|
||||
uses: numerique-gouv/action-trivy-cache@d6e94cfb488f03a0b3e8b8739aad94e74d24d8da # main
|
||||
continue-on-error: true
|
||||
with:
|
||||
docker-build-args: '-f src/summary/Dockerfile --target production'
|
||||
docker-image-name: '${{ env.DOCKER_CONTAINER_REGISTRY_HOSTNAME }}/${{ env.DOCKER_CONTAINER_REGISTRY_NAMESPACE }}/meet-summary:${{ github.sha }}'
|
||||
docker-context: './src/summary'
|
||||
-
|
||||
name: Build and push
|
||||
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
|
||||
with:
|
||||
context: ./src/summary
|
||||
file: ./src/summary/Dockerfile
|
||||
target: production
|
||||
platforms: ${{ env.BUILD_PLATFORMS }}
|
||||
build-args: DOCKER_USER=${{ env.DOCKER_USER }}:-1000
|
||||
push: ${{ github.event_name != 'pull_request' || startsWith(github.head_ref, 'integration/') }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
build-and-push-agents:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
-
|
||||
name: Checkout repository
|
||||
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
|
||||
-
|
||||
name: Set up QEMU
|
||||
if: env.IS_MULTI_PLATFORM_BUILD == 'true'
|
||||
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
||||
-
|
||||
name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
|
||||
with:
|
||||
images: lasuite/meet-agents
|
||||
-
|
||||
name: Login to DockerHub
|
||||
if: github.event_name != 'pull_request' || startsWith(github.head_ref, 'integration/')
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USER }}
|
||||
password: ${{ secrets.DOCKER_HUB_PASSWORD }}
|
||||
-
|
||||
name: Run trivy scan
|
||||
uses: numerique-gouv/action-trivy-cache@d6e94cfb488f03a0b3e8b8739aad94e74d24d8da # main
|
||||
continue-on-error: true
|
||||
with:
|
||||
docker-build-args: '-f src/agents/Dockerfile --target production'
|
||||
docker-image-name: '${{ env.DOCKER_CONTAINER_REGISTRY_HOSTNAME }}/${{ env.DOCKER_CONTAINER_REGISTRY_NAMESPACE }}/meet-agents:${{ github.sha }}'
|
||||
docker-context: './src/agents'
|
||||
-
|
||||
name: Build and push
|
||||
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
|
||||
with:
|
||||
context: ./src/agents
|
||||
file: ./src/agents/Dockerfile
|
||||
target: production
|
||||
platforms: ${{ env.BUILD_PLATFORMS }}
|
||||
build-args: DOCKER_USER=${{ env.DOCKER_USER }}:-1000
|
||||
push: ${{ github.event_name != 'pull_request' || startsWith(github.head_ref, 'integration/') }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-and-push:
|
||||
name: ${{ matrix.service }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- service: backend
|
||||
image_name: lasuite/meet-backend
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
target: backend-production
|
||||
- service: frontend
|
||||
image_name: lasuite/meet-frontend
|
||||
context: .
|
||||
file: ./src/frontend/Dockerfile
|
||||
target: frontend-production
|
||||
- service: frontend-dinum
|
||||
image_name: lasuite/meet-frontend-dinum
|
||||
context: .
|
||||
file: ./docker/dinum-frontend/Dockerfile
|
||||
target: frontend-production
|
||||
- service: summary
|
||||
image_name: lasuite/meet-summary
|
||||
context: ./src/summary
|
||||
file: ./src/summary/Dockerfile
|
||||
target: production
|
||||
- service: agents
|
||||
image_name: lasuite/meet-agents
|
||||
context: ./src/agents
|
||||
file: ./src/agents/Dockerfile
|
||||
target: production
|
||||
uses: suitenumerique/ci/.github/workflows/_docker-publish.yml@ca6401ac83e981e5b0a94f06f68baaa6e6d0a436 # v0.0.1
|
||||
with:
|
||||
image_name: ${{ matrix.image_name }}
|
||||
context: ${{ matrix.context }}
|
||||
file: ${{ matrix.file }}
|
||||
target: ${{ matrix.target }}
|
||||
docker_user: "1001:127"
|
||||
is_multi_platform: ${{ startsWith(github.ref, 'refs/tags/v') }}
|
||||
should_push: ${{ github.event_name != 'pull_request' || startsWith(github.head_ref, 'integration/') }}
|
||||
trivy_scan: true
|
||||
trivy_ignore_files: ./.github/.trivyignore
|
||||
secrets:
|
||||
DOCKER_HUB_USER: ${{ secrets.DOCKER_HUB_USER }}
|
||||
DOCKER_HUB_PASSWORD: ${{ secrets.DOCKER_HUB_PASSWORD }}
|
||||
|
||||
notify-argocd:
|
||||
permissions:
|
||||
contents: read
|
||||
needs:
|
||||
- build-and-push-frontend-generic
|
||||
- build-and-push-frontend-dinum
|
||||
- build-and-push-backend
|
||||
- build-and-push-summary
|
||||
- build-and-push-agents
|
||||
- build-and-push
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name != 'pull_request'
|
||||
steps:
|
||||
- uses: numerique-gouv/action-argocd-webhook-notification@cac2ee67896eb13e84e804f60c4271370424eaa8 # main
|
||||
- uses: suitenumerique/ci/actions/argocd-webhook-notification@ca6401ac83e981e5b0a94f06f68baaa6e6d0a436 # v0.0.1
|
||||
id: notify
|
||||
with:
|
||||
deployment_repo_path: "${{ secrets.DEPLOYMENT_REPO_URL }}"
|
||||
|
||||
@@ -1,33 +1,17 @@
|
||||
name: Release Chart
|
||||
run-name: Release Chart
|
||||
name: Release Helm chart
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- src/helm/meet/**
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
release:
|
||||
permissions:
|
||||
contents: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Cleanup
|
||||
run: rm -rf ./src/helm/extra
|
||||
|
||||
- name: Install Helm
|
||||
uses: azure/setup-helm@1a275c3b69536ee54be43f2070a358922e12c8d4 # v4.3.1
|
||||
env:
|
||||
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
||||
|
||||
- name: Publish Helm charts
|
||||
uses: numerique-gouv/helm-gh-pages@2cf477ae49d7c70037ceb1685803f4f7bad9b981 # add-overwrite-option
|
||||
with:
|
||||
charts_dir: ./src/helm
|
||||
linting: on
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
uses: suitenumerique/ci/.github/workflows/_release-helm-chart.yml@ca6401ac83e981e5b0a94f06f68baaa6e6d0a436 # v0.0.1
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
name: Security analysis
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- "**"
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
zizmor:
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
security-events: write
|
||||
uses: suitenumerique/ci/.github/workflows/_zizmor.yml@ca6401ac83e981e5b0a94f06f68baaa6e6d0a436 # v0.0.1
|
||||
with:
|
||||
config: .github/zizmor.yml
|
||||
@@ -0,0 +1,5 @@
|
||||
rules:
|
||||
unpinned-uses:
|
||||
config:
|
||||
policies:
|
||||
"suitenumerique/*": ref-pin
|
||||
+1
-11
@@ -12,11 +12,6 @@ and this project adheres to
|
||||
|
||||
- 🔒(backend) throttle meeting link generation
|
||||
- 🔒️(backend) add a daily cap on room creation
|
||||
- ✨(backend) add room soft-deletion to the external API
|
||||
|
||||
### Changed
|
||||
|
||||
- ♻️(backend) soft delete rooms instead of hard-delete
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -39,7 +34,6 @@ and this project adheres to
|
||||
- 🔧(compose) replace MinIO by Garage for local development
|
||||
- 🔧(helm) point media services to Garage by default
|
||||
|
||||
|
||||
### Fixed
|
||||
|
||||
- 🔒️(backend) fix critical and high CVEs in PyJWT
|
||||
@@ -59,7 +53,6 @@ and this project adheres to
|
||||
- ✨(backend) make the LiveKit default video codec configurable
|
||||
- 🔧(dev) add support for Bureautix workstations
|
||||
- ✨(frontend) add screen share zoom controls #1498
|
||||
- 🔨(makefile) add targets to list and download files stored in Garage
|
||||
|
||||
### Changed
|
||||
|
||||
@@ -78,9 +71,6 @@ and this project adheres to
|
||||
- ⬆️(addons) upgrade i18next from 26.4.0 to 26.4.2
|
||||
- 🔖(helm) release chart 0.0.28
|
||||
- ♻️(backend) decouple recording event handling from LiveKit egress statuses
|
||||
- ♻️(agents) replace the minio client by boto3
|
||||
- 🔧(compose) replace MinIO by Garage for local development
|
||||
- 🔧(helm) point media services to Garage by default
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -415,7 +405,7 @@ and this project adheres to
|
||||
|
||||
### Fixed
|
||||
|
||||
- ♿️(frontend) improve accessibilty of the Effects panel #1401
|
||||
- ♿️(frontend) improve accessibility of the Effects panel #1401
|
||||
|
||||
## [1.20.0] - 2026-06-12
|
||||
|
||||
|
||||
+8
-8
@@ -55,12 +55,12 @@ function _docker_compose() {
|
||||
function _dc_run() {
|
||||
_set_user
|
||||
|
||||
user_args="--user=$USER_ID"
|
||||
if [ -z $USER_ID ]; then
|
||||
user_args=""
|
||||
user_args=()
|
||||
if [ -n "$USER_ID" ]; then
|
||||
user_args=("--user=$USER_ID")
|
||||
fi
|
||||
|
||||
_docker_compose run --rm $user_args "$@"
|
||||
_docker_compose run --rm "${user_args[@]}" "$@"
|
||||
}
|
||||
|
||||
# _dc_exec: wrap docker compose exec command
|
||||
@@ -74,12 +74,12 @@ function _dc_exec() {
|
||||
|
||||
echo "🐳(compose) exec command: '\$@'"
|
||||
|
||||
user_args="--user=$USER_ID"
|
||||
if [ -z $USER_ID ]; then
|
||||
user_args=""
|
||||
user_args=()
|
||||
if [ -n "$USER_ID" ]; then
|
||||
user_args=("--user=$USER_ID")
|
||||
fi
|
||||
|
||||
_docker_compose exec $user_args "$@"
|
||||
_docker_compose exec "${user_args[@]}" "$@"
|
||||
}
|
||||
|
||||
# _django_manage: wrap django's manage.py command with docker compose
|
||||
|
||||
@@ -40,7 +40,7 @@ if [ -n "$CUSTOM_LOGO_URL" ]; then
|
||||
[[ "$IS_SVG" == false ]] && echo "[custom-logo] ERROR: not a valid SVG file" >&2 && exit 1
|
||||
|
||||
mv -f "$TMP_FILE" "$LOGO_FILE"
|
||||
echo "[custom-logo] INFO: Custom logo downloaded successfuly"
|
||||
echo "[custom-logo] INFO: Custom logo downloaded successfully"
|
||||
fi
|
||||
|
||||
mv src/backend/* ./
|
||||
|
||||
@@ -7,7 +7,7 @@ gunicorn -b 0.0.0.0:8000 meet.wsgi:application --log-file - &
|
||||
bin/run &
|
||||
|
||||
# if the current shell is killed, also terminate all its children
|
||||
trap "pkill SIGTERM -P $$" SIGTERM
|
||||
trap 'pkill -TERM -P $$' SIGTERM
|
||||
|
||||
# wait for a single child to finish,
|
||||
wait -n
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
set -o errexit
|
||||
|
||||
CURRENT_DIR=$(pwd)
|
||||
NAMESPACE=${1:-meet}
|
||||
SECRET_NAME=${2:-bitwarden-cli-meet}
|
||||
TEMP_SECRET_FILE=$(mktemp)
|
||||
@@ -30,10 +29,10 @@ check_secret_exists() {
|
||||
# Collect user input securely
|
||||
get_user_input() {
|
||||
echo "Please provide the following information:"
|
||||
read -p "Enter your Vaultwarden email login: " LOGIN
|
||||
read -s -p "Enter your Vaultwarden password: " PASSWORD
|
||||
read -r -p "Enter your Vaultwarden email login: " LOGIN
|
||||
read -r -s -p "Enter your Vaultwarden password: " PASSWORD
|
||||
echo
|
||||
read -p "Enter your Vaultwarden server url: " URL
|
||||
read -r -p "Enter your Vaultwarden server url: " URL
|
||||
}
|
||||
|
||||
# Create and apply the secret
|
||||
@@ -77,7 +76,7 @@ main() {
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo -e ${TEMP_SECRET_FILE}
|
||||
echo -e "${TEMP_SECRET_FILE}"
|
||||
|
||||
get_user_input
|
||||
echo -e "\nCreating Vaultwarden secret…"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
mkdir -p "$(dirname -- "${BASH_SOURCE[0]}")/../.git/hooks/"
|
||||
PRE_COMMIT_FILE="$(dirname -- "${BASH_SOURCE[0]}")/../.git/hooks/pre-commit"
|
||||
|
||||
cat <<'EOF' >$PRE_COMMIT_FILE
|
||||
cat <<'EOF' >"$PRE_COMMIT_FILE"
|
||||
#!/bin/bash
|
||||
|
||||
# directories containing potential secrets
|
||||
@@ -27,4 +27,4 @@ for d in $DIRS; do
|
||||
done
|
||||
EOF
|
||||
|
||||
chmod +x $PRE_COMMIT_FILE
|
||||
chmod +x "$PRE_COMMIT_FILE"
|
||||
|
||||
@@ -68,7 +68,7 @@ fi
|
||||
|
||||
# Ask user for release version number
|
||||
echo ""
|
||||
read -p "Enter release version number (e.g., 1.2.3): " VERSION
|
||||
read -r -p "Enter release version number (e.g., 1.2.3): " VERSION
|
||||
|
||||
# Validate version format (basic semver check)
|
||||
if ! [[ $VERSION =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
git submodule update --init --recursive
|
||||
# shellcheck disable=SC2016
|
||||
git submodule foreach 'git fetch origin; git checkout $(git rev-parse --abbrev-ref HEAD); git reset --hard origin/$(git rev-parse --abbrev-ref HEAD); git submodule update --recursive; git clean -dfx'
|
||||
|
||||
@@ -8,6 +8,6 @@ environments=$(awk '/environments:/ {flag=1; next} flag && NF {print} !NF {flag=
|
||||
|
||||
for env in $environments; do
|
||||
echo "################### $env lint ###################"
|
||||
helmfile -e $env -f src/helm/helmfile.yaml lint || exit 1
|
||||
helmfile -e "$env" -f src/helm/helmfile.yaml lint || exit 1
|
||||
echo -e "\n"
|
||||
done
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# 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
|
||||
# the proxy variables in their RUN steps. We need to pass the http_proxy explicitly
|
||||
# otherwise all connections fail during the build.
|
||||
|
||||
x-proxy-vars: &proxy-vars
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"extends": ["github>numerique-gouv/renovate-configuration"],
|
||||
"extends": ["github>suitenumerique/ci//renovate/default"],
|
||||
"dependencyDashboard": true,
|
||||
"labels": ["dependencies", "noChangeLog"],
|
||||
"packageRules": [
|
||||
|
||||
@@ -21,7 +21,7 @@ const { initI18n, translateUI } = require("../common/i18n");
|
||||
document.querySelector("#close-msg").style.display = "block";
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(`Error occured: ${e}`);
|
||||
console.error(`Error occurred: ${e}`);
|
||||
})
|
||||
.finally(() => {
|
||||
// NOTE: doesn't work with the desktop client — the browser considers
|
||||
|
||||
@@ -9,7 +9,6 @@ class AnalyticsEvent(StrEnum):
|
||||
# Rooms
|
||||
ROOM_CREATED = "room_created"
|
||||
ROOM_UPDATED = "room_updated"
|
||||
ROOM_DELETED = "room_deleted"
|
||||
|
||||
# Roomkit (meeting-room SIP devices)
|
||||
ROOMKIT_JOINED = "roomkit_joined"
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
"""Exceptions and guards shared by the API endpoints."""
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from rest_framework import exceptions, status
|
||||
|
||||
from core import models
|
||||
|
||||
|
||||
class RoomSoftDeleted(exceptions.APIException):
|
||||
"""Raised when the requested room has been soft deleted."""
|
||||
|
||||
status_code = status.HTTP_410_GONE
|
||||
default_detail = _("This room has been deleted.")
|
||||
default_code = "room_deleted"
|
||||
|
||||
|
||||
def ensure_room_not_deleted(resource):
|
||||
"""Raise a 410 Gone if the resource is a soft-deleted room.
|
||||
|
||||
Accepts a room or its parent resource, as referenced by accesses. Call it
|
||||
after permissions are checked, to avoid revealing room.
|
||||
"""
|
||||
if isinstance(resource, models.Room):
|
||||
room = resource
|
||||
else:
|
||||
room = getattr(resource, "room", None)
|
||||
|
||||
if room is not None and room.is_deleted:
|
||||
raise RoomSoftDeleted()
|
||||
@@ -20,7 +20,6 @@ from rest_framework.exceptions import PermissionDenied
|
||||
from timezone_field.rest_framework import TimeZoneSerializerField
|
||||
|
||||
from core import models, utils
|
||||
from core.api.exceptions import ensure_room_not_deleted
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -109,7 +108,6 @@ class ResourceAccessSerializerMixin:
|
||||
_("You must be administrator or owner of a room to add accesses to it.")
|
||||
)
|
||||
|
||||
ensure_room_not_deleted(resource)
|
||||
return resource
|
||||
|
||||
|
||||
|
||||
@@ -42,7 +42,6 @@ from rest_framework.settings import api_settings
|
||||
|
||||
from core import analytics, enums, models, utils
|
||||
from core.api import throttling
|
||||
from core.api.exceptions import ensure_room_not_deleted
|
||||
from core.api.filters import ListFileFilter
|
||||
from core.enums import MEDIA_STORAGE_URL_PATTERN
|
||||
from core.recording.enums import FileExtension
|
||||
@@ -76,7 +75,7 @@ from core.services.participants_management import (
|
||||
ParticipantsManagementException,
|
||||
)
|
||||
from core.services.room_creation import RoomCreation
|
||||
from core.services.room_management import RoomManagement, RoomManagementException
|
||||
from core.services.room_management import RoomManagement
|
||||
from core.services.room_roles import (
|
||||
RoomRoleError,
|
||||
RoomRoleService,
|
||||
@@ -193,9 +192,10 @@ class RoomViewSet(
|
||||
filter_kwargs = {"pk": self.kwargs["pk"]}
|
||||
except ValueError:
|
||||
filter_kwargs = {"slug": slugify(self.kwargs["pk"])}
|
||||
obj = get_object_or_404(models.Room.all_objects, **filter_kwargs)
|
||||
queryset = self.filter_queryset(self.get_queryset())
|
||||
obj = get_object_or_404(queryset, **filter_kwargs)
|
||||
# May raise a permission denied
|
||||
self.check_object_permissions(self.request, obj)
|
||||
ensure_room_not_deleted(obj)
|
||||
return obj
|
||||
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
@@ -247,18 +247,6 @@ class RoomViewSet(
|
||||
serializer = self.get_serializer(queryset, many=True)
|
||||
return drf_response.Response(serializer.data)
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
"""Soft delete the room and close its LiveKit room.
|
||||
|
||||
The room and its recordings are kept in database for traceability.
|
||||
"""
|
||||
try:
|
||||
RoomManagement.soft_delete(instance)
|
||||
except RoomManagementException as e:
|
||||
raise drf_exceptions.APIException(
|
||||
"Could not delete the room, please try again."
|
||||
) from e
|
||||
|
||||
def perform_create(self, serializer):
|
||||
"""Set the current user as owner of the newly created room.
|
||||
|
||||
@@ -943,13 +931,6 @@ class ResourceAccessViewSet(
|
||||
|
||||
return queryset
|
||||
|
||||
def get_object(self):
|
||||
"""Accesses to a soft-deleted room can be read but no longer modified."""
|
||||
access = super().get_object()
|
||||
if self.request.method not in drf_permissions.SAFE_METHODS:
|
||||
ensure_room_not_deleted(access.resource)
|
||||
return access
|
||||
|
||||
|
||||
class RecordingViewSet(
|
||||
mixins.DestroyModelMixin,
|
||||
|
||||
@@ -22,13 +22,11 @@ from rest_framework import (
|
||||
from rest_framework import (
|
||||
status as drf_status,
|
||||
)
|
||||
from rest_framework.generics import get_object_or_404
|
||||
|
||||
from core import analytics, api, models
|
||||
from core.api.exceptions import ensure_room_not_deleted
|
||||
from core.api.feature_flag import FeatureFlag
|
||||
from core.services.jwt_token import JwtTokenService
|
||||
from core.services.room_management import RoomManagement, RoomManagementException
|
||||
from core.services.room_management import RoomManagement
|
||||
|
||||
from ..services.provisional_user_service import (
|
||||
ProvisionalUserCreationDisabledError,
|
||||
@@ -144,7 +142,6 @@ class ApplicationViewSet(viewsets.ViewSet):
|
||||
|
||||
class RoomViewSet(
|
||||
mixins.CreateModelMixin,
|
||||
mixins.DestroyModelMixin,
|
||||
mixins.RetrieveModelMixin,
|
||||
mixins.ListModelMixin,
|
||||
mixins.UpdateModelMixin,
|
||||
@@ -162,13 +159,9 @@ class RoomViewSet(
|
||||
- create: Create a new room owned by the user (requires 'rooms:create' scope)
|
||||
- partial_update: Update a room's access level and configuration, for
|
||||
administrators and owners only (requires 'rooms:update' scope)
|
||||
- destroy: Soft delete a room and close its LiveKit room, for owners only
|
||||
(requires 'rooms:delete' scope)
|
||||
|
||||
Detail operations on a soft-deleted room answer 410 Gone.
|
||||
"""
|
||||
|
||||
http_method_names = ["get", "post", "patch", "delete", "head", "options"]
|
||||
http_method_names = ["get", "post", "patch", "head", "options"]
|
||||
|
||||
authentication_classes = [
|
||||
authentication.ApplicationJWTAuthentication,
|
||||
@@ -183,17 +176,6 @@ class RoomViewSet(
|
||||
queryset = models.Room.objects.all()
|
||||
serializer_class = serializers.RoomSerializer
|
||||
|
||||
def get_object(self):
|
||||
"""Get the room, answer 410 if it has been deleted.
|
||||
|
||||
Permissions are checked first so a deleted room is only revealed to
|
||||
users who would have been granted access to it.
|
||||
"""
|
||||
room = get_object_or_404(models.Room.all_objects, pk=self.kwargs["pk"])
|
||||
self.check_object_permissions(self.request, room)
|
||||
ensure_room_not_deleted(room)
|
||||
return room
|
||||
|
||||
def list(self, request, *args, **kwargs):
|
||||
"""Limit listed rooms to the ones related to the authenticated user."""
|
||||
|
||||
@@ -257,16 +239,6 @@ class RoomViewSet(
|
||||
|
||||
self._track_room_event(room, analytics.AnalyticsEvent.ROOM_CREATED)
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
"""Soft delete the room, close its LiveKit room, then log and track it."""
|
||||
try:
|
||||
RoomManagement.soft_delete(instance)
|
||||
except RoomManagementException as e:
|
||||
raise drf_exceptions.APIException(
|
||||
"Could not delete the room, please try again."
|
||||
) from e
|
||||
self._track_room_event(instance, analytics.AnalyticsEvent.ROOM_DELETED)
|
||||
|
||||
def perform_update(self, serializer: serializers.RoomSerializer):
|
||||
"""Persist the room update, sync it to LiveKit, then log and track it."""
|
||||
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
# Generated by Django 5.2.14 on 2026-09-16 10:00
|
||||
|
||||
import django.db.models.manager
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0024_room_last_started_at'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='room',
|
||||
name='deleted_at',
|
||||
field=models.DateTimeField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='room',
|
||||
options={'default_manager_name': 'all_objects', 'ordering': ('name',), 'verbose_name': 'Room', 'verbose_name_plural': 'Rooms'},
|
||||
),
|
||||
migrations.AlterModelManagers(
|
||||
name='room',
|
||||
managers=[
|
||||
('all_objects', django.db.models.manager.Manager()),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -406,33 +406,6 @@ class ResourceAccess(BaseModel):
|
||||
return super().delete(*args, **kwargs)
|
||||
|
||||
|
||||
class RoomQuerySet(models.QuerySet):
|
||||
"""QuerySet exposing the room lifecycle filters."""
|
||||
|
||||
def active(self):
|
||||
"""Rooms that have not been soft deleted."""
|
||||
return self.filter(deleted_at__isnull=True)
|
||||
|
||||
def deleted(self):
|
||||
"""Rooms that have been soft deleted."""
|
||||
return self.filter(deleted_at__isnull=False)
|
||||
|
||||
|
||||
class RoomManager(models.Manager.from_queryset(RoomQuerySet)):
|
||||
"""Manager hiding soft-deleted rooms, exposed as ``Room.objects``.
|
||||
|
||||
It is deliberately not the model's default manager: Django relies on
|
||||
``_default_manager`` for unique validation, which must see deleted rooms as
|
||||
they still guard their slug and pin code. Other object relations (e.g.
|
||||
``recording.room``) go through the base manager and still resolve deleted
|
||||
rooms, which keeps recordings and their notifications working.
|
||||
"""
|
||||
|
||||
def get_queryset(self):
|
||||
"""Exclude soft-deleted rooms."""
|
||||
return super().get_queryset().active()
|
||||
|
||||
|
||||
class Room(Resource):
|
||||
"""Model for one room"""
|
||||
|
||||
@@ -456,7 +429,6 @@ class Room(Resource):
|
||||
verbose_name=_("Visio room configuration"),
|
||||
help_text=_("Values for Visio parameters to configure the room."),
|
||||
)
|
||||
deleted_at = models.DateTimeField(null=True, blank=True)
|
||||
pin_code = models.CharField(
|
||||
max_length=None,
|
||||
unique=True,
|
||||
@@ -473,13 +445,8 @@ class Room(Resource):
|
||||
editable=False,
|
||||
)
|
||||
|
||||
# Managers
|
||||
objects = RoomManager()
|
||||
all_objects = models.Manager.from_queryset(RoomQuerySet)()
|
||||
|
||||
class Meta:
|
||||
db_table = "meet_room"
|
||||
default_manager_name = "all_objects"
|
||||
ordering = ("name",)
|
||||
verbose_name = _("Room")
|
||||
verbose_name_plural = _("Rooms")
|
||||
@@ -502,23 +469,6 @@ class Room(Resource):
|
||||
)
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
@property
|
||||
def is_deleted(self):
|
||||
"""Whether the room has been soft deleted."""
|
||||
return self.deleted_at is not None
|
||||
|
||||
def soft_delete(self):
|
||||
"""Soft delete the room.
|
||||
|
||||
The room is hidden from the default manager making it impossible to
|
||||
list, join or update.
|
||||
"""
|
||||
if self.deleted_at:
|
||||
raise RuntimeError("This room is already deleted.")
|
||||
|
||||
self.deleted_at = timezone.now()
|
||||
self.save(update_fields=["deleted_at"])
|
||||
|
||||
def clean_fields(self, exclude=None):
|
||||
"""
|
||||
Automatically generate the slug from the name and make sure it does not look like a UUID.
|
||||
@@ -554,7 +504,7 @@ class Room(Resource):
|
||||
|
||||
for _ in range(settings.ROOM_TELEPHONY_PIN_MAX_RETRIES):
|
||||
pin_code = str(secrets.randbelow(max_value)).zfill(length)
|
||||
if not Room.all_objects.filter(pin_code=pin_code).exists():
|
||||
if not Room.objects.filter(pin_code=pin_code).exists():
|
||||
return pin_code
|
||||
|
||||
# Log a warning as a temporary measure until backend observability is implemented.
|
||||
|
||||
@@ -24,7 +24,7 @@ class BaseEgressService:
|
||||
|
||||
def _get_filepath(self, filename: str, extension: str) -> str:
|
||||
"""Construct the file path for a given filename and extension.
|
||||
Unsecure method, doesn't handle paths robustly and securely.
|
||||
Insecure method, doesn't handle paths robustly and securely.
|
||||
"""
|
||||
return f"{self._config.output_folder}/{filename}.{extension}"
|
||||
|
||||
|
||||
@@ -271,24 +271,20 @@ class LiveKitEventsService:
|
||||
)
|
||||
raise ActionFailedError("Failed to process room started event") from e
|
||||
|
||||
try:
|
||||
room = models.Room.all_objects.get(id=room_id)
|
||||
except models.Room.DoesNotExist as err:
|
||||
raise ActionFailedError(f"Room with ID {room_id} does not exist") from err
|
||||
|
||||
# The block below is intended to fix the issue where long-lived livekit
|
||||
# tokens allow users who already entered a room to re-create it,
|
||||
# even if it was closed.
|
||||
if room.is_deleted:
|
||||
self._close_deleted_room(room_id)
|
||||
return
|
||||
|
||||
# Update through the queryset to skip the full_clean run by save()
|
||||
models.Room.all_objects.filter(pk=room.pk).update(
|
||||
room_updated_count = models.Room.objects.filter(pk=room_id).update(
|
||||
last_started_at=timezone.now()
|
||||
)
|
||||
if not room_updated_count:
|
||||
raise ActionFailedError(f"Room with ID {room_id} does not exist")
|
||||
|
||||
if settings.ROOM_TELEPHONY_ENABLED or settings.ROOMKIT_ENABLED:
|
||||
try:
|
||||
room = models.Room.objects.get(pk=room_id)
|
||||
except models.Room.DoesNotExist as err:
|
||||
raise ActionFailedError(
|
||||
f"Room with ID {room_id} does not exist"
|
||||
) from err
|
||||
|
||||
try:
|
||||
self.sip_management.ensure_dispatch_rule(room)
|
||||
except SIPException as e:
|
||||
@@ -296,25 +292,6 @@ class LiveKitEventsService:
|
||||
f"Failed to create sip dispatch rule for room {room_id}"
|
||||
) from e
|
||||
|
||||
@staticmethod
|
||||
def _close_deleted_room(room_id):
|
||||
"""Close a LiveKit room recreated after its room was soft deleted.
|
||||
|
||||
LiveKit auto-creates a room on join, so a participant still holding a
|
||||
valid token can bring a deleted room back to life until the token expires.
|
||||
"""
|
||||
|
||||
logger.warning(
|
||||
"LiveKit room %s started for a deleted room, closing it", room_id
|
||||
)
|
||||
|
||||
try:
|
||||
RoomManagement.delete_room(str(room_id))
|
||||
except RoomNotFoundException:
|
||||
logger.info("LiveKit room %s is already closed", room_id)
|
||||
except RoomManagementException as e:
|
||||
raise ActionFailedError(f"Failed to close deleted room {room_id}") from e
|
||||
|
||||
def _handle_room_finished(self, data):
|
||||
"""Handle 'room_finished' event."""
|
||||
|
||||
|
||||
@@ -6,8 +6,6 @@ import json
|
||||
from logging import getLogger
|
||||
from typing import Dict, Optional
|
||||
|
||||
from django.db import transaction
|
||||
|
||||
from asgiref.sync import async_to_sync
|
||||
from livekit.api import (
|
||||
DeleteRoomRequest,
|
||||
@@ -17,7 +15,6 @@ from livekit.api import (
|
||||
)
|
||||
|
||||
from core import utils
|
||||
from core.models import Room
|
||||
|
||||
logger = getLogger(__name__)
|
||||
|
||||
@@ -118,27 +115,6 @@ class RoomManagement:
|
||||
finally:
|
||||
await lkapi.aclose()
|
||||
|
||||
@classmethod
|
||||
def soft_delete(cls, room: Room):
|
||||
"""Soft delete a room and close its LiveKit room.
|
||||
|
||||
Raises:
|
||||
RoomManagementException: the LiveKit room could not be closed.
|
||||
"""
|
||||
|
||||
try:
|
||||
with transaction.atomic():
|
||||
room.soft_delete()
|
||||
try:
|
||||
cls.delete_room(str(room.id))
|
||||
except RoomNotFoundException:
|
||||
logger.info(
|
||||
"Room %s is not live in LiveKit, nothing to close", room.id
|
||||
)
|
||||
except RoomManagementException:
|
||||
room.deleted_at = None
|
||||
raise
|
||||
|
||||
@classmethod
|
||||
def sync_room_metadata(cls, room):
|
||||
"""Push a room's configuration and access level to its LiveKit room metadata.
|
||||
|
||||
@@ -27,7 +27,7 @@ def test_api_files_list_anonymous_not_allowed():
|
||||
|
||||
def test_api_files_list_authentificated_user_allowed():
|
||||
"""
|
||||
Authentificated users should be allowed to list files
|
||||
Authenticated users should be allowed to list files
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
client = APIClient()
|
||||
|
||||
@@ -116,29 +116,6 @@ def test_api_rooms_create_authenticated_existing_slug():
|
||||
assert response.json() == {"slug": ["Room with this Slug already exists."]}
|
||||
|
||||
|
||||
def test_api_rooms_create_authenticated_slug_held_by_soft_deleted_room():
|
||||
"""
|
||||
A deleted room keeps its slug: creating a room with the same name should
|
||||
fail validation rather than hit the database constraint.
|
||||
"""
|
||||
RoomFactory(name="my room").soft_delete()
|
||||
user = UserFactory()
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
response = client.post(
|
||||
"/api/v1.0/rooms/",
|
||||
{
|
||||
"name": "My Room!",
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
assert response.json() == {"slug": ["Room with this Slug already exists."]}
|
||||
assert Room.all_objects.count() == 1
|
||||
|
||||
|
||||
def test_api_rooms_create_authenticated_user_default_access_level():
|
||||
"""
|
||||
The user's default room access level should be applied to the new room
|
||||
|
||||
@@ -2,14 +2,11 @@
|
||||
Test rooms API endpoints in the Meet core app: delete.
|
||||
"""
|
||||
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from ...factories import RoomFactory, UserFactory
|
||||
from ...models import Room
|
||||
from ...services.room_management import RoomManagement, RoomNotFoundException
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
@@ -86,11 +83,10 @@ def test_api_rooms_delete_administrators():
|
||||
assert Room.objects.count() == 1
|
||||
|
||||
|
||||
@mock.patch.object(RoomManagement, "delete_room")
|
||||
def test_api_rooms_delete_owners(mock_delete_room):
|
||||
def test_api_rooms_delete_owners():
|
||||
"""
|
||||
Authenticated users should be able to delete a room for which they are directly
|
||||
owner. The room is soft deleted and its LiveKit room is closed.
|
||||
owner.
|
||||
"""
|
||||
user = UserFactory()
|
||||
room = RoomFactory(users=[(user, "owner")])
|
||||
@@ -103,71 +99,4 @@ def test_api_rooms_delete_owners(mock_delete_room):
|
||||
)
|
||||
|
||||
assert response.status_code == 204
|
||||
mock_delete_room.assert_called_once_with(str(room.id))
|
||||
assert Room.objects.exists() is False
|
||||
assert Room.all_objects.get(id=room.id).deleted_at is not None
|
||||
|
||||
|
||||
@mock.patch.object(
|
||||
RoomManagement,
|
||||
"delete_room",
|
||||
side_effect=RoomNotFoundException("Room does not exist"),
|
||||
)
|
||||
def test_api_rooms_delete_owners_room_not_live(mock_delete_room):
|
||||
"""
|
||||
Deleting a room that is not live in LiveKit should still soft delete it.
|
||||
"""
|
||||
user = UserFactory()
|
||||
room = RoomFactory(users=[(user, "owner")])
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
response = client.delete(
|
||||
f"/api/v1.0/rooms/{room.id}/",
|
||||
)
|
||||
|
||||
assert response.status_code == 204
|
||||
mock_delete_room.assert_called_once_with(str(room.id))
|
||||
assert Room.objects.exists() is False
|
||||
assert Room.all_objects.get(id=room.id).deleted_at is not None
|
||||
|
||||
|
||||
@mock.patch.object(RoomManagement, "delete_room")
|
||||
def test_api_rooms_delete_soft_deleted(mock_delete_room):
|
||||
"""Deleting a room that is already soft deleted should return a 410."""
|
||||
user = UserFactory()
|
||||
room = RoomFactory(users=[(user, "owner")])
|
||||
room.soft_delete()
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
response = client.delete(
|
||||
f"/api/v1.0/rooms/{room.id}/",
|
||||
)
|
||||
|
||||
assert response.status_code == 410
|
||||
assert response.json() == {"detail": "This room has been deleted."}
|
||||
mock_delete_room.assert_not_called()
|
||||
|
||||
|
||||
@mock.patch.object(RoomManagement, "delete_room")
|
||||
def test_api_rooms_delete_soft_deleted_not_owner(mock_delete_room):
|
||||
"""
|
||||
Deleting a soft-deleted room as a non-owner should return a 403,
|
||||
not revealing that the room has been deleted.
|
||||
"""
|
||||
user = UserFactory()
|
||||
room = RoomFactory(users=[(user, "administrator")])
|
||||
room.soft_delete()
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
response = client.delete(
|
||||
f"/api/v1.0/rooms/{room.id}/",
|
||||
)
|
||||
|
||||
assert response.status_code == 403
|
||||
mock_delete_room.assert_not_called()
|
||||
|
||||
@@ -458,7 +458,7 @@ def test_mute_participant_livekit_token_presence_check_twirp_error_forbidden(
|
||||
room = RoomFactory()
|
||||
|
||||
mock_livekit_client.room.get_participant.side_effect = TwirpError(
|
||||
msg="an error occured", code="not_found", status=500
|
||||
msg="an error occurred", code="not_found", status=500
|
||||
)
|
||||
|
||||
user = AnonymousUser()
|
||||
|
||||
@@ -188,28 +188,6 @@ def test_api_rooms_retrieve_anonymous_unregistered_not_allowed():
|
||||
assert response.json() == {"detail": "No Room matches the given query."}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("allow_unregistered_rooms", [True, False])
|
||||
@pytest.mark.parametrize("lookup", ["id", "slug"])
|
||||
@mock.patch("core.utils.generate_token", return_value="foo")
|
||||
def test_api_rooms_retrieve_soft_deleted(
|
||||
mock_token, lookup, allow_unregistered_rooms, settings
|
||||
):
|
||||
"""
|
||||
Retrieving a soft-deleted room should return a 410, and never fall back
|
||||
to an unregistered room with the same slug.
|
||||
"""
|
||||
settings.ALLOW_UNREGISTERED_ROOMS = allow_unregistered_rooms
|
||||
room = RoomFactory(access_level=RoomAccessLevel.PUBLIC)
|
||||
room.soft_delete()
|
||||
|
||||
client = APIClient()
|
||||
response = client.get(f"/api/v1.0/rooms/{getattr(room, lookup)!s}/")
|
||||
|
||||
assert response.status_code == 410
|
||||
assert response.json() == {"detail": "This room has been deleted."}
|
||||
mock_token.assert_not_called()
|
||||
|
||||
|
||||
@mock.patch("core.utils.generate_token", return_value="foo")
|
||||
@override_settings(
|
||||
LIVEKIT_CONFIGURATION={
|
||||
|
||||
@@ -7,7 +7,6 @@ import uuid
|
||||
from unittest import mock
|
||||
|
||||
from django.conf import settings
|
||||
from django.test.utils import override_settings
|
||||
|
||||
import pytest
|
||||
from livekit.api import AccessToken, TwirpError, VideoGrants
|
||||
@@ -118,8 +117,7 @@ def test_start_subtitle_invalid_token():
|
||||
assert response.json() == {"detail": "Invalid LiveKit token: Not enough segments"}
|
||||
|
||||
|
||||
@override_settings(ROOM_SUBTITLE_ENABLED=False)
|
||||
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
|
||||
|
||||
@@ -27,10 +27,7 @@ from core.services.livekit_events import (
|
||||
to_recording_event,
|
||||
)
|
||||
from core.services.lobby import LobbyService
|
||||
from core.services.room_management import (
|
||||
RoomManagementException,
|
||||
RoomNotFoundException,
|
||||
)
|
||||
from core.services.room_management import RoomManagementException
|
||||
from core.services.sip_management import (
|
||||
SIPException,
|
||||
SIPManagement,
|
||||
@@ -799,61 +796,6 @@ def test_handle_room_started_raises_error_for_nonexistent_room(service):
|
||||
service._handle_room_started(mock_data)
|
||||
|
||||
|
||||
@mock.patch.object(SIPManagement, "ensure_dispatch_rule")
|
||||
@mock.patch("core.services.room_management.RoomManagement.delete_room")
|
||||
def test_handle_room_started_closes_deleted_room(
|
||||
mock_delete_room, mock_ensure_dispatch_rule, service, settings
|
||||
):
|
||||
"""Should close a LiveKit room recreated for a soft-deleted room."""
|
||||
settings.ROOM_TELEPHONY_ENABLED = True
|
||||
room = RoomFactory()
|
||||
room.soft_delete()
|
||||
mock_data = mock.MagicMock()
|
||||
mock_data.room.name = str(room.id)
|
||||
|
||||
service._handle_room_started(mock_data)
|
||||
|
||||
mock_delete_room.assert_called_once_with(str(room.id))
|
||||
mock_ensure_dispatch_rule.assert_not_called()
|
||||
|
||||
|
||||
@mock.patch(
|
||||
"core.services.room_management.RoomManagement.delete_room",
|
||||
side_effect=RoomNotFoundException("Room does not exist"),
|
||||
)
|
||||
def test_handle_room_started_ignores_already_closed_deleted_room(
|
||||
mock_delete_room, service
|
||||
):
|
||||
"""Should proceed silently when the deleted room is already closed in LiveKit."""
|
||||
room = RoomFactory()
|
||||
room.soft_delete()
|
||||
mock_data = mock.MagicMock()
|
||||
mock_data.room.name = str(room.id)
|
||||
|
||||
service._handle_room_started(mock_data)
|
||||
|
||||
mock_delete_room.assert_called_once_with(str(room.id))
|
||||
|
||||
|
||||
@mock.patch(
|
||||
"core.services.room_management.RoomManagement.delete_room",
|
||||
side_effect=RoomManagementException("Could not delete room"),
|
||||
)
|
||||
def test_handle_room_started_raises_error_when_closing_deleted_room_fails(
|
||||
mock_delete_room, service
|
||||
):
|
||||
"""Should raise ActionFailedError when the deleted room cannot be closed."""
|
||||
room = RoomFactory()
|
||||
room.soft_delete()
|
||||
mock_data = mock.MagicMock()
|
||||
mock_data.room.name = str(room.id)
|
||||
|
||||
expected_error = f"Failed to close deleted room {room.id}"
|
||||
|
||||
with pytest.raises(ActionFailedError, match=expected_error):
|
||||
service._handle_room_started(mock_data)
|
||||
|
||||
|
||||
@mock.patch.object(
|
||||
api.WebhookReceiver, "receive", side_effect=Exception("Invalid payload")
|
||||
)
|
||||
|
||||
@@ -6,7 +6,7 @@ import pytest
|
||||
from livekit.api import TwirpError
|
||||
|
||||
from core.factories import RoomFactory
|
||||
from core.models import Room, RoomAccessLevel
|
||||
from core.models import RoomAccessLevel
|
||||
from core.services.room_management import (
|
||||
RoomManagement,
|
||||
RoomManagementException,
|
||||
@@ -62,29 +62,6 @@ def test_delete_room_raises_management_exception(mock_create_livekit_client):
|
||||
mock_api.aclose.assert_awaited_once()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@mock.patch.object(RoomManagement, "delete_room")
|
||||
def test_soft_delete_failure_rolls_back_and_can_be_retried(mock_delete_room):
|
||||
"""A failed soft delete leaves the room untouched, in database and in memory,
|
||||
so it can be retried."""
|
||||
room = RoomFactory()
|
||||
mock_delete_room.side_effect = RoomManagementException("Could not delete room")
|
||||
|
||||
with pytest.raises(RoomManagementException):
|
||||
RoomManagement.soft_delete(room)
|
||||
|
||||
assert room.deleted_at is None
|
||||
assert Room.objects.filter(id=room.id).exists()
|
||||
|
||||
mock_delete_room.side_effect = None
|
||||
RoomManagement.soft_delete(room)
|
||||
|
||||
assert mock_delete_room.call_count == 2
|
||||
assert room.deleted_at is not None
|
||||
assert Room.all_objects.get(id=room.id).deleted_at is not None
|
||||
assert Room.objects.filter(id=room.id).exists() is False
|
||||
|
||||
|
||||
@mock.patch.object(RoomManagement, "update_metadata")
|
||||
def test_sync_room_metadata_pushes_configuration_and_access_level(mock_update_metadata):
|
||||
"""The room's configuration and access level are forwarded to LiveKit."""
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
Test SIP mamagement service.
|
||||
Test SIP management service.
|
||||
"""
|
||||
|
||||
# pylint: disable=W0212
|
||||
|
||||
@@ -956,99 +956,3 @@ def test_api_room_user_access_delete_owners_last_owner():
|
||||
|
||||
assert response.status_code == 403
|
||||
assert ResourceAccess.objects.count() == 1
|
||||
|
||||
|
||||
# Soft-deleted rooms
|
||||
|
||||
|
||||
def test_api_room_user_accesses_create_soft_deleted_room():
|
||||
"""
|
||||
Owners of a soft-deleted room should not be allowed to add accesses to it.
|
||||
"""
|
||||
user = UserFactory()
|
||||
room = RoomFactory(users=[(user, "owner")])
|
||||
room.soft_delete()
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
response = client.post(
|
||||
"/api/v1.0/resource-accesses/",
|
||||
{
|
||||
"user": str(UserFactory().id),
|
||||
"resource": str(room.id),
|
||||
"role": "member",
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 410
|
||||
assert response.json() == {"detail": "This room has been deleted."}
|
||||
assert ResourceAccess.objects.count() == 1
|
||||
|
||||
|
||||
def test_api_room_user_accesses_create_soft_deleted_room_not_administrator():
|
||||
"""
|
||||
Users without privileges on a soft-deleted room should get a 403,
|
||||
not revealing that the room has been deleted.
|
||||
"""
|
||||
user = UserFactory()
|
||||
room = RoomFactory(users=[(user, "member")])
|
||||
room.soft_delete()
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
response = client.post(
|
||||
"/api/v1.0/resource-accesses/",
|
||||
{
|
||||
"user": str(UserFactory().id),
|
||||
"resource": str(room.id),
|
||||
"role": "member",
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 403
|
||||
assert ResourceAccess.objects.count() == 1
|
||||
|
||||
|
||||
def test_api_room_user_accesses_update_soft_deleted_room():
|
||||
"""
|
||||
Owners of a soft-deleted room should not be allowed to update its accesses.
|
||||
"""
|
||||
user = UserFactory()
|
||||
room = RoomFactory(users=[(user, "owner")])
|
||||
access = UserResourceAccessFactory(resource=room, role="member")
|
||||
room.soft_delete()
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
response = client.patch(
|
||||
f"/api/v1.0/resource-accesses/{access.id!s}/",
|
||||
{"role": "administrator"},
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == 410
|
||||
access.refresh_from_db()
|
||||
assert access.role == "member"
|
||||
|
||||
|
||||
def test_api_room_user_access_delete_soft_deleted_room():
|
||||
"""
|
||||
Owners of a soft-deleted room should not be allowed to remove its accesses.
|
||||
"""
|
||||
user = UserFactory()
|
||||
room = RoomFactory(users=[(user, "owner")])
|
||||
access = UserResourceAccessFactory(resource=room, role="member")
|
||||
room.soft_delete()
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
response = client.delete(
|
||||
f"/api/v1.0/resource-accesses/{access.id!s}/",
|
||||
)
|
||||
|
||||
assert response.status_code == 410
|
||||
assert ResourceAccess.objects.filter(id=access.id).exists() is True
|
||||
|
||||
@@ -26,11 +26,7 @@ from core.models import (
|
||||
RoomAccessLevel,
|
||||
User,
|
||||
)
|
||||
from core.services.room_management import (
|
||||
RoomManagement,
|
||||
RoomManagementException,
|
||||
RoomNotFoundException,
|
||||
)
|
||||
from core.services.room_management import RoomManagement
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
@@ -567,53 +563,6 @@ def test_api_rooms_retrieve_not_found():
|
||||
assert "no room matches the given query." in str(response.data).lower()
|
||||
|
||||
|
||||
def test_api_rooms_retrieve_invalid_id():
|
||||
"""Retrieving a room with a malformed id should return a 404."""
|
||||
|
||||
user = UserFactory()
|
||||
token = generate_test_token(user, [ApplicationScope.ROOMS_RETRIEVE])
|
||||
|
||||
client = APIClient()
|
||||
client.credentials(HTTP_AUTHORIZATION=f"Bearer {token}")
|
||||
response = client.get("/external-api/v1.0/rooms/not-a-uuid/")
|
||||
|
||||
assert response.status_code == 404
|
||||
|
||||
|
||||
def test_api_rooms_retrieve_soft_deleted():
|
||||
"""Retrieving a soft-deleted room should return a 410."""
|
||||
|
||||
user = UserFactory()
|
||||
room = RoomFactory(users=[(user, RoleChoices.OWNER)])
|
||||
room.soft_delete()
|
||||
|
||||
token = generate_test_token(user, [ApplicationScope.ROOMS_RETRIEVE])
|
||||
|
||||
client = APIClient()
|
||||
client.credentials(HTTP_AUTHORIZATION=f"Bearer {token}")
|
||||
response = client.get(f"/external-api/v1.0/rooms/{room.id}/")
|
||||
|
||||
assert response.status_code == 410
|
||||
assert response.json() == {"detail": "This room has been deleted."}
|
||||
|
||||
|
||||
def test_api_rooms_retrieve_soft_deleted_not_member():
|
||||
"""Retrieving a soft-deleted room without any role on it should return a 403,
|
||||
not revealing that the room has been deleted."""
|
||||
|
||||
user = UserFactory()
|
||||
room = RoomFactory()
|
||||
room.soft_delete()
|
||||
|
||||
token = generate_test_token(user, [ApplicationScope.ROOMS_RETRIEVE])
|
||||
|
||||
client = APIClient()
|
||||
client.credentials(HTTP_AUTHORIZATION=f"Bearer {token}")
|
||||
response = client.get(f"/external-api/v1.0/rooms/{room.id}/")
|
||||
|
||||
assert response.status_code == 403
|
||||
|
||||
|
||||
def test_api_rooms_create_requires_authentication():
|
||||
"""Creating rooms without authentication should return 401."""
|
||||
|
||||
@@ -1443,204 +1392,6 @@ def test_api_rooms_update_tracks_analytics(mock_update_metadata, mock_capture):
|
||||
mock_update_metadata.assert_called_once()
|
||||
|
||||
|
||||
@mock.patch.object(RoomManagement, "delete_room")
|
||||
def test_api_rooms_delete_requires_authentication(mock_delete_room):
|
||||
"""Deleting a room without authentication should return 401."""
|
||||
|
||||
room = RoomFactory(users=[(UserFactory(), RoleChoices.OWNER)])
|
||||
|
||||
client = APIClient()
|
||||
response = client.delete(f"/external-api/v1.0/rooms/{room.id}/")
|
||||
|
||||
assert response.status_code == 401
|
||||
assert Room.objects.filter(id=room.id).exists() is True
|
||||
mock_delete_room.assert_not_called()
|
||||
|
||||
|
||||
@mock.patch.object(RoomManagement, "delete_room")
|
||||
def test_api_rooms_delete_requires_scope(mock_delete_room):
|
||||
"""Deleting a room requires the ROOMS_DELETE scope."""
|
||||
|
||||
user = UserFactory()
|
||||
room = RoomFactory(users=[(user, RoleChoices.OWNER)])
|
||||
|
||||
# Token without ROOMS_DELETE scope
|
||||
token = generate_test_token(
|
||||
user, [ApplicationScope.ROOMS_RETRIEVE, ApplicationScope.ROOMS_UPDATE]
|
||||
)
|
||||
|
||||
client = APIClient()
|
||||
client.credentials(HTTP_AUTHORIZATION=f"Bearer {token}")
|
||||
response = client.delete(f"/external-api/v1.0/rooms/{room.id}/")
|
||||
|
||||
assert response.status_code == 403
|
||||
assert (
|
||||
"insufficient permissions. required scope: rooms:delete"
|
||||
in str(response.data).lower()
|
||||
)
|
||||
assert Room.objects.filter(id=room.id).exists() is True
|
||||
mock_delete_room.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("role", [RoleChoices.ADMIN, RoleChoices.MEMBER, None])
|
||||
@mock.patch.object(RoomManagement, "delete_room")
|
||||
def test_api_rooms_delete_without_ownership(mock_delete_room, role):
|
||||
"""Only owners should be able to delete a room, administrators included."""
|
||||
|
||||
user = UserFactory()
|
||||
users = [(user, role)] if role else []
|
||||
room = RoomFactory(users=users)
|
||||
|
||||
token = generate_test_token(user, [ApplicationScope.ROOMS_DELETE])
|
||||
|
||||
client = APIClient()
|
||||
client.credentials(HTTP_AUTHORIZATION=f"Bearer {token}")
|
||||
response = client.delete(f"/external-api/v1.0/rooms/{room.id}/")
|
||||
|
||||
assert response.status_code == 403
|
||||
assert Room.objects.filter(id=room.id).exists() is True
|
||||
mock_delete_room.assert_not_called()
|
||||
|
||||
|
||||
@mock.patch.object(RoomManagement, "delete_room")
|
||||
def test_api_rooms_delete_unknown_room(mock_delete_room):
|
||||
"""Deleting a room that does not exist should return 404."""
|
||||
|
||||
user = UserFactory()
|
||||
token = generate_test_token(user, [ApplicationScope.ROOMS_DELETE])
|
||||
|
||||
client = APIClient()
|
||||
client.credentials(HTTP_AUTHORIZATION=f"Bearer {token}")
|
||||
response = client.delete(f"/external-api/v1.0/rooms/{uuid.uuid4()}/")
|
||||
|
||||
assert response.status_code == 404
|
||||
mock_delete_room.assert_not_called()
|
||||
|
||||
|
||||
@mock.patch("core.external_api.viewsets.analytics.capture")
|
||||
@mock.patch.object(RoomManagement, "delete_room")
|
||||
def test_api_rooms_delete_success(mock_delete_room, mock_capture):
|
||||
"""Owners should be able to delete a room: it is soft deleted, its LiveKit
|
||||
room is closed and a ROOM_DELETED analytics event is emitted."""
|
||||
|
||||
user = UserFactory()
|
||||
room = RoomFactory(
|
||||
users=[(user, RoleChoices.OWNER)], access_level=RoomAccessLevel.TRUSTED
|
||||
)
|
||||
|
||||
token = generate_test_token(user, [ApplicationScope.ROOMS_DELETE])
|
||||
application = Application.objects.get()
|
||||
|
||||
client = APIClient()
|
||||
client.credentials(HTTP_AUTHORIZATION=f"Bearer {token}")
|
||||
response = client.delete(f"/external-api/v1.0/rooms/{room.id}/")
|
||||
|
||||
assert response.status_code == 204
|
||||
mock_delete_room.assert_called_once_with(str(room.id))
|
||||
assert Room.objects.filter(id=room.id).exists() is False
|
||||
assert Room.all_objects.get(id=room.id).deleted_at is not None
|
||||
|
||||
mock_capture.assert_called_once()
|
||||
captured_user, event, properties = mock_capture.call_args[0]
|
||||
|
||||
assert captured_user == user
|
||||
assert event == AnalyticsEvent.ROOM_DELETED
|
||||
assert properties == {
|
||||
"room_id": str(room.pk),
|
||||
"access_level": RoomAccessLevel.TRUSTED,
|
||||
"client_id": str(application.client_id),
|
||||
"external_api": True,
|
||||
"auth_method": "ApplicationJWTAuthentication",
|
||||
"$set": {"email": user.email},
|
||||
}
|
||||
|
||||
|
||||
@mock.patch("core.external_api.viewsets.analytics.capture")
|
||||
@mock.patch.object(
|
||||
RoomManagement,
|
||||
"delete_room",
|
||||
side_effect=RoomManagementException("Could not delete room"),
|
||||
)
|
||||
def test_api_rooms_delete_livekit_failure(mock_delete_room, mock_capture):
|
||||
"""When the LiveKit room can't be closed, the deletion should be rolled back
|
||||
and no analytics event emitted."""
|
||||
|
||||
user = UserFactory()
|
||||
room = RoomFactory(users=[(user, RoleChoices.OWNER)])
|
||||
|
||||
token = generate_test_token(user, [ApplicationScope.ROOMS_DELETE])
|
||||
|
||||
client = APIClient()
|
||||
client.credentials(HTTP_AUTHORIZATION=f"Bearer {token}")
|
||||
response = client.delete(f"/external-api/v1.0/rooms/{room.id}/")
|
||||
|
||||
assert response.status_code == 500
|
||||
assert response.json() == {"detail": "Could not delete the room, please try again."}
|
||||
mock_delete_room.assert_called_once_with(str(room.id))
|
||||
assert Room.objects.get(id=room.id).deleted_at is None
|
||||
mock_capture.assert_not_called()
|
||||
|
||||
|
||||
@mock.patch.object(
|
||||
RoomManagement,
|
||||
"delete_room",
|
||||
side_effect=RoomNotFoundException("Room does not exist"),
|
||||
)
|
||||
def test_api_rooms_delete_room_not_live(mock_delete_room):
|
||||
"""Deleting a room that is not live in LiveKit should still soft delete it."""
|
||||
|
||||
user = UserFactory()
|
||||
room = RoomFactory(users=[(user, RoleChoices.OWNER)])
|
||||
|
||||
token = generate_test_token(user, [ApplicationScope.ROOMS_DELETE])
|
||||
|
||||
client = APIClient()
|
||||
client.credentials(HTTP_AUTHORIZATION=f"Bearer {token}")
|
||||
response = client.delete(f"/external-api/v1.0/rooms/{room.id}/")
|
||||
|
||||
assert response.status_code == 204
|
||||
mock_delete_room.assert_called_once_with(str(room.id))
|
||||
assert Room.objects.filter(id=room.id).exists() is False
|
||||
assert Room.all_objects.get(id=room.id).deleted_at is not None
|
||||
|
||||
|
||||
@mock.patch.object(RoomManagement, "delete_room")
|
||||
def test_api_rooms_delete_soft_deleted(mock_delete_room):
|
||||
"""Deleting a room that is already soft deleted should return a 410."""
|
||||
|
||||
user = UserFactory()
|
||||
room = RoomFactory(users=[(user, RoleChoices.OWNER)])
|
||||
room.soft_delete()
|
||||
|
||||
token = generate_test_token(user, [ApplicationScope.ROOMS_DELETE])
|
||||
|
||||
client = APIClient()
|
||||
client.credentials(HTTP_AUTHORIZATION=f"Bearer {token}")
|
||||
response = client.delete(f"/external-api/v1.0/rooms/{room.id}/")
|
||||
|
||||
assert response.status_code == 410
|
||||
mock_delete_room.assert_not_called()
|
||||
|
||||
|
||||
@mock.patch.object(RoomManagement, "delete_room")
|
||||
def test_api_rooms_delete_soft_deleted_not_owner(mock_delete_room):
|
||||
"""Deleting a soft-deleted room as a non-owner should return a 403,
|
||||
not revealing that the room has been deleted."""
|
||||
|
||||
user = UserFactory()
|
||||
room = RoomFactory(users=[(user, RoleChoices.ADMIN)])
|
||||
room.soft_delete()
|
||||
|
||||
token = generate_test_token(user, [ApplicationScope.ROOMS_DELETE])
|
||||
|
||||
client = APIClient()
|
||||
client.credentials(HTTP_AUTHORIZATION=f"Bearer {token}")
|
||||
response = client.delete(f"/external-api/v1.0/rooms/{room.id}/")
|
||||
|
||||
assert response.status_code == 403
|
||||
mock_delete_room.assert_not_called()
|
||||
|
||||
|
||||
def test_api_rooms_response_no_url(settings):
|
||||
"""Response should not include url field when APPLICATION_BASE_URL is None."""
|
||||
settings.APPLICATION_BASE_URL = None
|
||||
|
||||
@@ -121,7 +121,7 @@ const config: Config = {
|
||||
},
|
||||
tokens: defineTokens({
|
||||
/* we take a few things from the panda preset but for now we clear out some stuff.
|
||||
* This way we'll only add the things we need step by step and prevent using lots of differents things.
|
||||
* This way we'll only add the things we need step by step and prevent using lots of different things.
|
||||
*/
|
||||
...pandaPreset.theme.tokens,
|
||||
colors: defineTokens.colors({
|
||||
|
||||
@@ -12,8 +12,6 @@ export enum ApiLobbyStatus {
|
||||
DENIED = 'denied',
|
||||
TIMEOUT = 'timeout',
|
||||
ACCEPTED = 'accepted',
|
||||
// Client-side only: the room was deleted while waiting
|
||||
DELETED = 'deleted',
|
||||
}
|
||||
|
||||
export interface ApiRequestEntry {
|
||||
|
||||
@@ -85,7 +85,6 @@ export const Conference = ({
|
||||
const {
|
||||
status: fetchStatus,
|
||||
isError: isFetchError,
|
||||
error: fetchError,
|
||||
data,
|
||||
} = useQuery({
|
||||
queryKey: fetchKey,
|
||||
@@ -99,8 +98,6 @@ export const Conference = ({
|
||||
if (error.statusCode == '404') {
|
||||
createRoom({ slug: roomId, username })
|
||||
}
|
||||
// A deleted room can't be recreated, surface the error instead
|
||||
if (error.statusCode == '410') throw error
|
||||
}),
|
||||
retry: false,
|
||||
})
|
||||
@@ -161,7 +158,7 @@ export const Conference = ({
|
||||
*
|
||||
* Issue: On Firefox behind proxy configurations, WebSocket signaling fails to establish.
|
||||
* Symptom: Client receives HTTP 200 instead of expected 101 (Switching Protocols).
|
||||
* Root Cause: Certificate/security issue where the initial request is considered unsecure.
|
||||
* Root Cause: Certificate/security issue where the initial request is considered insecure.
|
||||
*
|
||||
* Solution: Pre-establish a WebSocket connection to the signaling server, which fails.
|
||||
* This "primes" the connection, allowing subsequent WebSocket establishments to work correctly.
|
||||
@@ -201,15 +198,6 @@ export const Conference = ({
|
||||
}, [apiConfig?.livekit])
|
||||
|
||||
const { t } = useTranslation('rooms')
|
||||
if (fetchError?.statusCode == 410) {
|
||||
return (
|
||||
<ErrorScreen
|
||||
title={t('error.deletedRoom.heading')}
|
||||
body={t('error.deletedRoom.body')}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
if (isCreateError) {
|
||||
// this error screen should be replaced by a proper waiting room for anonymous user.
|
||||
return (
|
||||
@@ -294,7 +282,6 @@ export const Conference = ({
|
||||
return
|
||||
case DisconnectReason.DUPLICATE_IDENTITY:
|
||||
case DisconnectReason.PARTICIPANT_REMOVED:
|
||||
case DisconnectReason.ROOM_DELETED:
|
||||
navigateTo(
|
||||
'feedback',
|
||||
{},
|
||||
|
||||
@@ -74,9 +74,7 @@ export const Lobby = ({
|
||||
const { openLoginHint } = useLoginHint()
|
||||
|
||||
const handleSubmit = async () => {
|
||||
const { data, error } = await refetchRoom()
|
||||
|
||||
if (error?.statusCode == 410) return
|
||||
const { data } = await refetchRoom()
|
||||
|
||||
if (!data?.livekit) {
|
||||
// Display a message to inform the user that by logging in, they won't have to wait for room entry approval.
|
||||
@@ -90,22 +88,6 @@ export const Lobby = ({
|
||||
enterRoom()
|
||||
}
|
||||
|
||||
if (
|
||||
status === ApiLobbyStatus.DELETED ||
|
||||
(isError && error?.statusCode == 410)
|
||||
) {
|
||||
return (
|
||||
<VStack alignItems="center" textAlign="center">
|
||||
<H lvl={1} margin={false} centered>
|
||||
{t('deleted.title')}
|
||||
</H>
|
||||
<Text as="p" variant="note">
|
||||
{t('deleted.body')}
|
||||
</Text>
|
||||
</VStack>
|
||||
)
|
||||
}
|
||||
|
||||
switch (status) {
|
||||
case ApiLobbyStatus.TIMEOUT:
|
||||
return (
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { keys } from '@/api/queryKeys'
|
||||
import { ApiError } from '@/api/ApiError'
|
||||
import {
|
||||
requestEntry,
|
||||
ApiLobbyStatus,
|
||||
@@ -40,21 +39,10 @@ export const useLobby = ({
|
||||
const { data: waitingData } = useQuery({
|
||||
queryKey: [keys.requestEntry, roomId],
|
||||
queryFn: async () => {
|
||||
let response: ApiRequestEntry
|
||||
try {
|
||||
response = await requestEntry({
|
||||
roomId,
|
||||
username,
|
||||
})
|
||||
} catch (error) {
|
||||
// The room was deleted while waiting, stop polling
|
||||
if (error instanceof ApiError && error.statusCode === 410) {
|
||||
clearWaitingTimeout()
|
||||
setStatus(ApiLobbyStatus.DELETED)
|
||||
return { status: ApiLobbyStatus.DELETED }
|
||||
}
|
||||
throw error
|
||||
}
|
||||
const response = await requestEntry({
|
||||
roomId,
|
||||
username,
|
||||
})
|
||||
if (response.status === ApiLobbyStatus.ACCEPTED) {
|
||||
clearWaitingTimeout()
|
||||
setStatus(ApiLobbyStatus.ACCEPTED)
|
||||
|
||||
@@ -31,7 +31,6 @@ const buttonClass = css({
|
||||
enum DisconnectReasonKey {
|
||||
DuplicateIdentity = 'duplicateIdentity',
|
||||
ParticipantRemoved = 'participantRemoved',
|
||||
RoomDeleted = 'roomDeleted',
|
||||
}
|
||||
|
||||
const FeedbackRoute = () => {
|
||||
@@ -47,8 +46,6 @@ const FeedbackRoute = () => {
|
||||
return DisconnectReasonKey.DuplicateIdentity
|
||||
case DisconnectReason.PARTICIPANT_REMOVED:
|
||||
return DisconnectReasonKey.ParticipantRemoved
|
||||
case DisconnectReason.ROOM_DELETED:
|
||||
return DisconnectReasonKey.RoomDeleted
|
||||
}
|
||||
}, [])
|
||||
|
||||
@@ -59,10 +56,7 @@ const FeedbackRoute = () => {
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Rejoining is not possible once removed or once the room is deleted
|
||||
const showBackButton =
|
||||
reasonKey !== DisconnectReasonKey.ParticipantRemoved &&
|
||||
reasonKey !== DisconnectReasonKey.RoomDeleted
|
||||
const showBackButton = reasonKey !== DisconnectReasonKey.ParticipantRemoved
|
||||
|
||||
return (
|
||||
<Screen layout="centered" footer={false}>
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
"heading": {
|
||||
"normal": "Du hast das Meeting verlassen",
|
||||
"duplicateIdentity": "Du bist dem Meeting von einem anderen Gerät aus beigetreten",
|
||||
"participantRemoved": "Du wurdest vom Host aus dem Meeting entfernt",
|
||||
"roomDeleted": "Dieses Meeting wurde gelöscht"
|
||||
"participantRemoved": "Du wurdest vom Host aus dem Meeting entfernt"
|
||||
},
|
||||
"home": "Zur Startseite zurückkehren",
|
||||
"back": "Dem Meeting erneut beitreten"
|
||||
@@ -95,10 +94,6 @@
|
||||
"timeoutInvite": {
|
||||
"title": "Du kannst diesem Meeting nicht beitreten",
|
||||
"body": "Niemand hat auf deine Anfrage reagiert"
|
||||
},
|
||||
"deleted": {
|
||||
"title": "Du kannst diesem Meeting nicht beitreten",
|
||||
"body": "Dieses Meeting wurde gelöscht."
|
||||
}
|
||||
},
|
||||
"leaveRoomPrompt": "Hiermit verlässt du das Meeting.",
|
||||
@@ -220,10 +215,6 @@
|
||||
"heading": "Authentifizierung erforderlich",
|
||||
"body": "Dieser Raum wurde noch nicht erstellt. Bitte authentifiziere dich, um ihn zu erstellen, oder warte, bis eine authentifizierte Person dies tut."
|
||||
},
|
||||
"deletedRoom": {
|
||||
"heading": "Meeting gelöscht",
|
||||
"body": "Dieses Meeting wurde gelöscht und kann nicht mehr betreten werden."
|
||||
},
|
||||
"screenShare": {
|
||||
"title": "Bildschirmfreigabe nicht möglich",
|
||||
"ariaLabel": "Bildschirmfreigabe nicht möglich",
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
"heading": {
|
||||
"normal": "You have left the meeting",
|
||||
"duplicateIdentity": "You have joined the meeting from another device",
|
||||
"participantRemoved": "You have been removed from the meeting by a host",
|
||||
"roomDeleted": "This meeting has been deleted"
|
||||
"participantRemoved": "You have been removed from the meeting by a host"
|
||||
},
|
||||
"home": "Return to home",
|
||||
"back": "Rejoin the meeting"
|
||||
@@ -95,10 +94,6 @@
|
||||
"timeoutInvite": {
|
||||
"title": "You cannot join this call",
|
||||
"body": "No one responded to your request"
|
||||
},
|
||||
"deleted": {
|
||||
"title": "You cannot join this call",
|
||||
"body": "This meeting has been deleted."
|
||||
}
|
||||
},
|
||||
"leaveRoomPrompt": "This will make you leave the meeting.",
|
||||
@@ -220,10 +215,6 @@
|
||||
"heading": "Authentication Required",
|
||||
"body": "This room has not been created yet. Please authenticate to create it or wait for an authenticated user to do so."
|
||||
},
|
||||
"deletedRoom": {
|
||||
"heading": "Meeting deleted",
|
||||
"body": "This meeting has been deleted and can no longer be joined."
|
||||
},
|
||||
"screenShare": {
|
||||
"title": "Unable to share your screen",
|
||||
"ariaLabel": "Unable to share your screen",
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
"heading": {
|
||||
"normal": "Has salido de la reunión",
|
||||
"duplicateIdentity": "Te has unido a la reunión desde otro dispositivo",
|
||||
"participantRemoved": "Un administrador te ha expulsado de la llamada",
|
||||
"roomDeleted": "Esta reunión ha sido eliminada"
|
||||
"participantRemoved": "Un administrador te ha expulsado de la llamada"
|
||||
},
|
||||
"home": "Volver al inicio",
|
||||
"back": "Volver a la reunión"
|
||||
@@ -95,10 +94,6 @@
|
||||
"timeoutInvite": {
|
||||
"title": "No puedes participar en esta llamada",
|
||||
"body": "Nadie ha respondido a tu solicitud de participación en la llamada"
|
||||
},
|
||||
"deleted": {
|
||||
"title": "No puedes participar en esta llamada",
|
||||
"body": "Esta reunión ha sido eliminada."
|
||||
}
|
||||
},
|
||||
"leaveRoomPrompt": "Volver al inicio hará que salgas de la reunión.",
|
||||
@@ -220,10 +215,6 @@
|
||||
"heading": "Autenticación necesaria",
|
||||
"body": "Esta reunión todavía no se ha creado. Autentícate para crearla o espera a que lo haga un usuario autenticado."
|
||||
},
|
||||
"deletedRoom": {
|
||||
"heading": "Reunión eliminada",
|
||||
"body": "Esta reunión ha sido eliminada y ya no es posible unirse a ella."
|
||||
},
|
||||
"screenShare": {
|
||||
"title": "No se puede compartir tu pantalla",
|
||||
"ariaLabel": "No se puede compartir tu pantalla",
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
"heading": {
|
||||
"normal": "Vous avez quitté la réunion",
|
||||
"duplicateIdentity": "Vous avez rejoint la réunion depuis un autre appareil",
|
||||
"participantRemoved": "Vous avez été exclu de l'appel par un administrateur",
|
||||
"roomDeleted": "Cette réunion a été supprimée"
|
||||
"participantRemoved": "Vous avez été exclu de l'appel par un administrateur"
|
||||
},
|
||||
"home": "Retourner à l'accueil",
|
||||
"back": "Réintégrer la réunion"
|
||||
@@ -95,10 +94,6 @@
|
||||
"timeoutInvite": {
|
||||
"title": "Vous ne pouvez pas participer à cet appel",
|
||||
"body": "Personne n'a répondu à votre demande de participation à l'appel"
|
||||
},
|
||||
"deleted": {
|
||||
"title": "Vous ne pouvez pas participer à cet appel",
|
||||
"body": "Cette réunion a été supprimée."
|
||||
}
|
||||
},
|
||||
"leaveRoomPrompt": "Revenir à l'accueil vous fera quitter la réunion.",
|
||||
@@ -220,10 +215,6 @@
|
||||
"heading": "Authentification requise",
|
||||
"body": "Cette réunion n'a pas encore été créée. Veuillez vous authentifier pour la créer ou attendre qu'un utilisateur authentifié le fasse."
|
||||
},
|
||||
"deletedRoom": {
|
||||
"heading": "Réunion supprimée",
|
||||
"body": "Cette réunion a été supprimée et n'est plus accessible."
|
||||
},
|
||||
"screenShare": {
|
||||
"title": "Impossible de partager votre écran",
|
||||
"ariaLabel": "Impossible de partager votre écran",
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
"heading": {
|
||||
"normal": "Je hebt de vergadering verlaten",
|
||||
"duplicateIdentity": "U heeft de vergadering via een ander apparaat geopend",
|
||||
"participantRemoved": "U bent door een beheerder uit het gesprek verwijderd",
|
||||
"roomDeleted": "Deze vergadering is verwijderd"
|
||||
"participantRemoved": "U bent door een beheerder uit het gesprek verwijderd"
|
||||
},
|
||||
"home": "Keer terug naar het hoofdscherm",
|
||||
"back": "Sluit weer bij de vergadering aan"
|
||||
@@ -95,10 +94,6 @@
|
||||
"timeoutInvite": {
|
||||
"title": "U kunt niet deelnemen aan dit gesprek",
|
||||
"body": "Niemand heeft gereageerd op uw verzoek om deel te nemen aan het gesprek"
|
||||
},
|
||||
"deleted": {
|
||||
"title": "U kunt niet deelnemen aan dit gesprek",
|
||||
"body": "Deze vergadering is verwijderd."
|
||||
}
|
||||
},
|
||||
"leaveRoomPrompt": "Dat zal u de vergadering doen verlaten.",
|
||||
@@ -220,10 +215,6 @@
|
||||
"heading": "Verificatie vereist",
|
||||
"body": "Deze ruimte is nog niet gemaakt. Logt u alstublieft in om hem aan te maken, of wacht tot een ingelogde gebruiker dat doet."
|
||||
},
|
||||
"deletedRoom": {
|
||||
"heading": "Vergadering verwijderd",
|
||||
"body": "Deze vergadering is verwijderd en u kunt er niet meer aan deelnemen."
|
||||
},
|
||||
"screenShare": {
|
||||
"title": "Kan uw scherm niet delen",
|
||||
"ariaLabel": "Kan uw scherm niet delen",
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
docker image ls | grep readme-generator-for-helm
|
||||
if [ "$?" -ne "0" ]; then
|
||||
if ! docker image ls | grep readme-generator-for-helm; then
|
||||
git clone https://github.com/bitnami/readme-generator-for-helm.git /tmp/readme-generator-for-helm
|
||||
cd /tmp/readme-generator-for-helm
|
||||
cd /tmp/readme-generator-for-helm || exit 1
|
||||
docker build -t readme-generator-for-helm:latest .
|
||||
cd $(dirname -- "${BASH_SOURCE[0]}")
|
||||
cd "$(dirname -- "${BASH_SOURCE[0]}")" || exit 1
|
||||
fi
|
||||
docker run --rm -it -v .:/source -w /source readme-generator-for-helm:latest readme-generator -v values.yaml -r README.md
|
||||
|
||||
@@ -51,7 +51,7 @@ app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
transform dictionnary of environment variables
|
||||
transform dictionary of environment variables
|
||||
Usage : {{ include "meet.env.transformDict" .Values.envVars }}
|
||||
|
||||
Example:
|
||||
|
||||
Reference in New Issue
Block a user