mirror of
https://github.com/suitenumerique/meet.git
synced 2026-10-07 14:00:56 +00:00
Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2ac457c43e | |||
| e9b523b522 | |||
| 70d348337e | |||
| 258722adfd | |||
| 0a6724e7ad | |||
| c1c2d93932 | |||
| 9efb9577c4 | |||
| 2137c6b444 | |||
| aa01733f6b | |||
| 03db669e51 | |||
| 97a73bf4f2 | |||
| 267a2265d9 | |||
| 14507d57a9 |
@@ -88,6 +88,8 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
|
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Create writable /data
|
- name: Create writable /data
|
||||||
run: |
|
run: |
|
||||||
@@ -159,6 +161,8 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
|
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Install ffmpeg
|
- name: Install ffmpeg
|
||||||
run: |
|
run: |
|
||||||
@@ -186,6 +190,8 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
|
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: cd src/frontend/ && npm ci --ignore-scripts
|
run: cd src/frontend/ && npm ci --ignore-scripts
|
||||||
@@ -196,6 +202,27 @@ jobs:
|
|||||||
- name: Check format
|
- name: Check format
|
||||||
run: cd src/frontend/ && npm run check
|
run: cd src/frontend/ && npm run check
|
||||||
|
|
||||||
|
test-front:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Install Node.js
|
||||||
|
uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
|
||||||
|
with:
|
||||||
|
node-version: "24"
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: cd src/frontend/ && npm ci --ignore-scripts
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: cd src/frontend/ && npm test
|
||||||
|
|
||||||
lint-sdk:
|
lint-sdk:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
@@ -206,6 +233,8 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
|
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci --ignore-scripts
|
run: npm ci --ignore-scripts
|
||||||
@@ -227,6 +256,8 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
|
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci --ignore-scripts
|
run: npm ci --ignore-scripts
|
||||||
|
|||||||
+5
-1
@@ -8,6 +8,8 @@ and this project adheres to
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [1.34.0] - 2026-10-07
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- ✨(helm) import environment variables from Secrets and ConfigMaps
|
- ✨(helm) import environment variables from Secrets and ConfigMaps
|
||||||
@@ -16,7 +18,8 @@ and this project adheres to
|
|||||||
- 🔧(summary) add setting to control Sentry traces sampling rate
|
- 🔧(summary) add setting to control Sentry traces sampling rate
|
||||||
- ✨(frontend) let signed-out visitors start a meeting
|
- ✨(frontend) let signed-out visitors start a meeting
|
||||||
- ✨(backend) expose `allow_unregistered_rooms` in the frontend configuration
|
- ✨(backend) expose `allow_unregistered_rooms` in the frontend configuration
|
||||||
- ✨(backend) authenticate external API calls with Menshen exchanged tokens
|
- ✅(frontend) add vitest so the frontend can carry unit tests
|
||||||
|
- ♿️(frontend) make participant pagination readable and keyboard reachable #1775
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
@@ -166,6 +169,7 @@ and this project adheres to
|
|||||||
### Added
|
### Added
|
||||||
|
|
||||||
- ✨(any) let any authenticated user manage the lobby on trusted rooms
|
- ✨(any) let any authenticated user manage the lobby on trusted rooms
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- 📱(frontend) collapse mobile control bar items on narrow viewports
|
- 📱(frontend) collapse mobile control bar items on narrow viewports
|
||||||
|
|||||||
@@ -169,7 +169,7 @@ run-summary: ## start only the summary application and all needed services
|
|||||||
@$(COMPOSE) up --force-recreate -d celery-summary-summarize
|
@$(COMPOSE) up --force-recreate -d celery-summary-summarize
|
||||||
.PHONY: run-summary
|
.PHONY: run-summary
|
||||||
|
|
||||||
run-agents: ## start the multi-user-transcriber agent
|
run-agents: ## start the LiveKit agents (opt-in, see docs/developping_locally.md)
|
||||||
@$(MAKE) run-agent-multi-user-transcriber
|
@$(MAKE) run-agent-multi-user-transcriber
|
||||||
@$(MAKE) run-agent-metadata-collector
|
@$(MAKE) run-agent-metadata-collector
|
||||||
.PHONY: run-agents
|
.PHONY: run-agents
|
||||||
@@ -186,7 +186,6 @@ run:
|
|||||||
run: ## start the wsgi (production) and development server
|
run: ## start the wsgi (production) and development server
|
||||||
@$(MAKE) run-backend
|
@$(MAKE) run-backend
|
||||||
@$(MAKE) run-summary
|
@$(MAKE) run-summary
|
||||||
@$(MAKE) run-agents
|
|
||||||
@$(COMPOSE) up --force-recreate -d frontend
|
@$(COMPOSE) up --force-recreate -d frontend
|
||||||
.PHONY: run
|
.PHONY: run
|
||||||
|
|
||||||
@@ -260,7 +259,8 @@ lint-pylint: ## lint back-end python sources with pylint only on changed files f
|
|||||||
test: ## run project tests; pass extra pytest args via ARGS, e.g. `make test ARGS="-vv"`
|
test: ## run project tests; pass extra pytest args via ARGS, e.g. `make test ARGS="-vv"`
|
||||||
@args="$(ARGS) $(filter-out $@,$(MAKECMDGOALS))" && \
|
@args="$(ARGS) $(filter-out $@,$(MAKECMDGOALS))" && \
|
||||||
$(MAKE) test-back-parallel ARGS="$${args}" && \
|
$(MAKE) test-back-parallel ARGS="$${args}" && \
|
||||||
$(MAKE) test-summary ARGS="$${args}"
|
$(MAKE) test-summary ARGS="$${args}" && \
|
||||||
|
$(MAKE) test-frontend
|
||||||
.PHONY: test
|
.PHONY: test
|
||||||
|
|
||||||
test-back: ## run back-end tests (pass extra pytest args via ARGS)
|
test-back: ## run back-end tests (pass extra pytest args via ARGS)
|
||||||
@@ -278,6 +278,10 @@ test-summary: ## run summary tests (pass extra pytest args via ARGS)
|
|||||||
bin/pytest-summary $${args}
|
bin/pytest-summary $${args}
|
||||||
.PHONY: test-summary
|
.PHONY: test-summary
|
||||||
|
|
||||||
|
test-frontend: ## run the frontend unit tests
|
||||||
|
cd $(PATH_FRONT) && npm test
|
||||||
|
.PHONY: test-frontend
|
||||||
|
|
||||||
makemigrations: ## run django makemigrations for the Meet project.
|
makemigrations: ## run django makemigrations for the Meet project.
|
||||||
@echo "$(BOLD)Running makemigrations$(RESET)"
|
@echo "$(BOLD)Running makemigrations$(RESET)"
|
||||||
@$(COMPOSE) up -d postgresql
|
@$(COMPOSE) up -d postgresql
|
||||||
|
|||||||
+76
-25
@@ -16,43 +16,54 @@ the following command inside your docker container:
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
### Purging inactive rooms
|
### Marketing / Brevo integration now uses `django-lasuite`
|
||||||
|
|
||||||
Rooms now keep track of the last time they were started (`last_started_at`), fed by LiveKit's `room_started` webhook. A new `purge_inactive_rooms` management command permanently deletes the rooms that have not been started for `ROOM_INACTIVITY_DELETION_DAYS` days. See [the room purge documentation](docs/features/room-purge.md).
|
The in-house marketing service (`core.services.marketing`) has been removed and
|
||||||
|
replaced by the shared implementation from `django-lasuite`
|
||||||
|
(`lasuite.marketing`). This fixes a bug where updating a user's contact on
|
||||||
|
Brevo overwrote their list memberships, removing lists set by other
|
||||||
|
La Suite products. Existing lists are now preserved and merged.
|
||||||
|
|
||||||
- The feature is **disabled by default**: nothing is deleted unless you set `ROOM_INACTIVITY_DELETION_DAYS`.
|
**Celery worker required.** Newsletter signup on login
|
||||||
- The migration marks every existing room as started at the time of the upgrade, so no existing room can be purged before a full inactivity period has elapsed after upgrading.
|
(`SIGNUP_NEW_USER_TO_MARKETING_EMAIL=True`) is now dispatched as an
|
||||||
- Rooms holding a saved recording their users may still access are kept: any saved recording, or, when `RECORDING_EXPIRATION_DAYS` is set, a saved recording created within that window.
|
asynchronous Celery task (`lasuite.marketing.tasks.create_or_update_contact`)
|
||||||
- Inactivity is measured from LiveKit's `room_started` webhook: if it is not delivered to your backend, rooms in daily use look inactive and get purged.
|
instead of a synchronous call with a 1s timeout. Make sure a Celery worker is
|
||||||
- When a room is purged, all it's configuration and access rights are also deleted. Its slug becomes available again and can be reused when a meeting is created from that same URL.
|
running alongside the backend, otherwise contacts will never be pushed to Brevo.
|
||||||
|
|
||||||
* With `ALLOW_UNREGISTERED_ROOMS=false`, only an authenticated user can navigate to a previously existing link after the room has been purged. Doing so recreates the room in the database with a fresh configuration, with that user associated with it and granted admin rights.
|
**Configuration changes.** The following environment variables / settings are
|
||||||
* With `ALLOW_UNREGISTERED_ROOMS=true`, any user can reopen the purged room by navigating to the same URL. In that case, the room is created dynamically and no corresponding room entry is persisted in the database.
|
**removed** and no longer read:
|
||||||
|
|
||||||
### Local development: MinIO replaced by Garage
|
- `MARKETING_SERVICE_CLASS`
|
||||||
|
- `BREVO_API_KEY`
|
||||||
|
- `BREVO_API_CONTACT_LIST_IDS`
|
||||||
|
- `BREVO_API_CONTACT_ATTRIBUTES` (previous default: `{"VISIO_USER": True}`)
|
||||||
|
- `BREVO_API_TIMEOUT`
|
||||||
|
|
||||||
The development stacks now use [Garage](https://garagehq.deuxfleurs.fr/) instead of MinIO as S3 storage. Garage keeps its own format in `data/media/meta` and `data/media/data` and cannot read what MinIO left there, so local recordings and files will be lost.
|
They are replaced by a single `LASUITE_MARKETING` setting, configured through:
|
||||||
|
|
||||||
To migrate a local environment:
|
| Variable | Default | Description |
|
||||||
|
| ------------------------------ | ------------------------------------------------ | -------------------------------------------- |
|
||||||
|
| `LASUITE_MARKETING_BACKEND` | `lasuite.marketing.backends.dummy.DummyBackend` | Backend class path |
|
||||||
|
| `LASUITE_MARKETING_PARAMETERS` | `{}` | Keyword arguments passed to the backend |
|
||||||
|
|
||||||
1. Stop the stack and remove its containers, including the former `minio` one: `docker compose down --remove-orphans`
|
⚠️ The default backend is now a **dummy** (no-op). If you previously used
|
||||||
2. Optionally reclaim the space used by MinIO: `rm -rf data/media && make data/media`
|
Brevo, you must explicitly configure it, otherwise signups are silently dropped:
|
||||||
3. In your `env.d/development/*` files, replace `minio:9000` by `garage:9000`, the `meet` / `password` credentials by `meet-access-key` / `meet-secret-access-key`, and add `AWS_S3_REGION_NAME=local` (or delete these files and run `make create-env-files`)
|
|
||||||
4. Run `make create-env-files` to generate `env.d/development/garage`, which holds a random RPC secret for Garage.
|
|
||||||
5. Rebuild the images, since the summary and agent images now install boto3 instead of minio
|
|
||||||
|
|
||||||
### Summary service and metadata collector: boto3 replaces the minio client
|
LASUITE_MARKETING_BACKEND=lasuite.marketing.backends.brevo.BrevoBackend
|
||||||
|
LASUITE_MARKETING_PARAMETERS={"api_key": "<your-brevo-api-key>", "api_contact_list_ids": [1, 2], "api_contact_attributes": {"VISIO_USER": True}}
|
||||||
|
|
||||||
The summary service and the metadata collector agent now talk to S3 through boto3 instead of the minio client, with the same settings.
|
Migration mapping:
|
||||||
Requests are now signed for `AWS_S3_REGION_NAME` as-is. When it is not set, the region is no longer looked up from the bucket: boto3 falls back to `AWS_DEFAULT_REGION`, then to `us-east-1`. If you left `AWS_S3_REGION_NAME` unset, set it to your provider's region before upgrading, or providers that check the signing region will reject the transcripts, summaries and meeting metadata uploads, as well as their signed URLs.
|
|
||||||
|
|
||||||
Also:
|
- `BREVO_API_KEY` → `api_key`
|
||||||
- Signed URLs to transcripts and summaries are now always path-style (`<endpoint>/<bucket>/<key>`), whereas the minio client used virtual-hosted-style URLs
|
- `BREVO_API_CONTACT_LIST_IDS` → `api_contact_list_ids`
|
||||||
- The metadata collector now accepts `AWS_S3_ENDPOINT_URL` with or without a scheme, like the summary service: the scheme always follows `AWS_S3_SECURE_ACCESS`.
|
- `BREVO_API_CONTACT_ATTRIBUTES` → `api_contact_attributes` (re-add
|
||||||
|
`{"VISIO_USER": True}` if you relied on the old default)
|
||||||
|
- `BREVO_API_TIMEOUT` → no equivalent (the request runs in a background task)
|
||||||
|
|
||||||
### Helm chart: media services default to Garage
|
Note: `BREVO_API_KEY` used to support being read from a secret file; the API key
|
||||||
|
now lives inside `LASUITE_MARKETING_PARAMETERS`, so adapt how you inject that
|
||||||
|
secret (e.g. build the whole variable from your secret store).
|
||||||
|
|
||||||
The `meet` chart now defaults `serviceMedia.host` and `serviceMediaFiles.host` to `garage.meet.svc.cluster.local`, and the `upstream-vhost` annotation of `ingressMedia` and `ingressMediaFiles` to `garage.meet.svc.cluster.local:9000`. If you relied on the former `minio.meet.svc.cluster.local` defaults, set these values explicitly to your S3 service before upgrading, or recordings and files stop being served under `/media`.
|
|
||||||
### Recording encoding settings replaced by a resolution/profile model
|
### Recording encoding settings replaced by a resolution/profile model
|
||||||
|
|
||||||
The `RECORDING_ENCODING_*` settings introduced in v1.16.0 exposed raw encoder
|
The `RECORDING_ENCODING_*` settings introduced in v1.16.0 exposed raw encoder
|
||||||
@@ -175,6 +186,46 @@ Before enabling it:
|
|||||||
See [docs/features/recording.md](docs/features/recording.md#tuning-recording-encoding)
|
See [docs/features/recording.md](docs/features/recording.md#tuning-recording-encoding)
|
||||||
for the full setting reference, the shipped profile table and the tuning caveats.
|
for the full setting reference, the shipped profile table and the tuning caveats.
|
||||||
|
|
||||||
|
## v1.33.0
|
||||||
|
|
||||||
|
### Purging inactive rooms
|
||||||
|
|
||||||
|
Rooms now keep track of the last time they were started (`last_started_at`), fed by LiveKit's `room_started` webhook. A new `purge_inactive_rooms` management command permanently deletes the rooms that have not been started for `ROOM_INACTIVITY_DELETION_DAYS` days. See [the room purge documentation](docs/features/room-purge.md).
|
||||||
|
|
||||||
|
- The feature is **disabled by default**: nothing is deleted unless you set `ROOM_INACTIVITY_DELETION_DAYS`.
|
||||||
|
- The migration marks every existing room as started at the time of the upgrade, so no existing room can be purged before a full inactivity period has elapsed after upgrading.
|
||||||
|
- Rooms holding a saved recording their users may still access are kept: any saved recording, or, when `RECORDING_EXPIRATION_DAYS` is set, a saved recording created within that window.
|
||||||
|
- Inactivity is measured from LiveKit's `room_started` webhook: if it is not delivered to your backend, rooms in daily use look inactive and get purged.
|
||||||
|
- When a room is purged, all it's configuration and access rights are also deleted. Its slug becomes available again and can be reused when a meeting is created from that same URL.
|
||||||
|
|
||||||
|
* With `ALLOW_UNREGISTERED_ROOMS=false`, only an authenticated user can navigate to a previously existing link after the room has been purged. Doing so recreates the room in the database with a fresh configuration, with that user associated with it and granted admin rights.
|
||||||
|
* With `ALLOW_UNREGISTERED_ROOMS=true`, any user can reopen the purged room by navigating to the same URL. In that case, the room is created dynamically and no corresponding room entry is persisted in the database.
|
||||||
|
|
||||||
|
### Local development: MinIO replaced by Garage
|
||||||
|
|
||||||
|
The development stacks now use [Garage](https://garagehq.deuxfleurs.fr/) instead of MinIO as S3 storage. Garage keeps its own format in `data/media/meta` and `data/media/data` and cannot read what MinIO left there, so local recordings and files will be lost.
|
||||||
|
|
||||||
|
To migrate a local environment:
|
||||||
|
|
||||||
|
1. Stop the stack and remove its containers, including the former `minio` one: `docker compose down --remove-orphans`
|
||||||
|
2. Optionally reclaim the space used by MinIO: `rm -rf data/media && make data/media`
|
||||||
|
3. In your `env.d/development/*` files, replace `minio:9000` by `garage:9000`, the `meet` / `password` credentials by `meet-access-key` / `meet-secret-access-key`, and add `AWS_S3_REGION_NAME=local` (or delete these files and run `make create-env-files`)
|
||||||
|
4. Run `make create-env-files` to generate `env.d/development/garage`, which holds a random RPC secret for Garage.
|
||||||
|
5. Rebuild the images, since the summary and agent images now install boto3 instead of minio
|
||||||
|
|
||||||
|
### Summary service and metadata collector: boto3 replaces the minio client
|
||||||
|
|
||||||
|
The summary service and the metadata collector agent now talk to S3 through boto3 instead of the minio client, with the same settings.
|
||||||
|
Requests are now signed for `AWS_S3_REGION_NAME` as-is. When it is not set, the region is no longer looked up from the bucket: boto3 falls back to `AWS_DEFAULT_REGION`. If you left `AWS_S3_REGION_NAME` unset, set it to your provider's region before upgrading, or providers that check the signing region will reject the transcripts, summaries and meeting metadata uploads, as well as their signed URLs.
|
||||||
|
|
||||||
|
Also:
|
||||||
|
- Signed URLs to transcripts and summaries are now always path-style (`<endpoint>/<bucket>/<key>`), whereas the minio client used virtual-hosted-style URLs
|
||||||
|
- The metadata collector now accepts `AWS_S3_ENDPOINT_URL` with or without a scheme, like the summary service: the scheme always follows `AWS_S3_SECURE_ACCESS`.
|
||||||
|
|
||||||
|
### Helm chart: media services default to Garage
|
||||||
|
|
||||||
|
The `meet` chart now defaults `serviceMedia.host` and `serviceMediaFiles.host` to `garage.meet.svc.cluster.local`, and the `upstream-vhost` annotation of `ingressMedia` and `ingressMediaFiles` to `garage.meet.svc.cluster.local:9000`. If you relied on the former `minio.meet.svc.cluster.local` defaults, set these values explicitly to your S3 service before upgrading, or recordings and files stop being served under `/media`.
|
||||||
|
|
||||||
## v1.30.0
|
## v1.30.0
|
||||||
|
|
||||||
### Removing S3 storage-event webhooks for recordings
|
### Removing S3 storage-event webhooks for recordings
|
||||||
|
|||||||
+9
-8
@@ -104,15 +104,16 @@ k8s_yaml(secret_yaml_generic(
|
|||||||
|
|
||||||
k8s_yaml(local('cd ../src/helm && helmfile -n meet -e ${DEV_ENV:-dev-keycloak} template .'))
|
k8s_yaml(local('cd ../src/helm && helmfile -n meet -e ${DEV_ENV:-dev-keycloak} template .'))
|
||||||
|
|
||||||
k8s_resource('garage-cors', resource_deps=['garage'])
|
k8s_resource('dev-backend-garage-cors', resource_deps=['dev-backend-garage'])
|
||||||
k8s_resource('meet-backend', resource_deps=['postgresql', 'garage-cors', 'redis', 'livekit-livekit-server'])
|
k8s_resource('dev-backend-keycloak', resource_deps=['dev-backend-keycloak-pg'])
|
||||||
k8s_resource('meet-celery-backend', resource_deps=['redis'])
|
k8s_resource('meet-backend', resource_deps=['dev-backend-postgres', 'dev-backend-garage-cors', 'dev-backend-redis', 'dev-backend-keycloak', 'livekit-livekit-server'])
|
||||||
k8s_resource('meet-celery-summarize', resource_deps=['redis'])
|
k8s_resource('meet-celery-backend', resource_deps=['dev-backend-redis'])
|
||||||
k8s_resource('meet-celery-summary-backend', resource_deps=['redis'])
|
k8s_resource('meet-celery-summarize', resource_deps=['dev-backend-redis'])
|
||||||
k8s_resource('meet-celery-transcribe-default', resource_deps=['redis'])
|
k8s_resource('meet-celery-summary-backend', resource_deps=['dev-backend-redis'])
|
||||||
k8s_resource('livekit-livekit-server', resource_deps=['redis'])
|
k8s_resource('meet-celery-transcribe-default', resource_deps=['dev-backend-redis'])
|
||||||
|
k8s_resource('livekit-livekit-server', resource_deps=['dev-backend-redis'])
|
||||||
k8s_resource('livekit-livekit-server-test-connection', resource_deps=['livekit-livekit-server'])
|
k8s_resource('livekit-livekit-server-test-connection', resource_deps=['livekit-livekit-server'])
|
||||||
k8s_resource('keycloak', resource_deps=['kc-postgresql'])
|
k8s_resource('livekit-egress', resource_deps=['livekit-livekit-server'])
|
||||||
# Trigger once on launch
|
# Trigger once on launch
|
||||||
k8s_resource(
|
k8s_resource(
|
||||||
'meet-backend-createsuperuser',
|
'meet-backend-createsuperuser',
|
||||||
|
|||||||
+5
-8
@@ -9,6 +9,8 @@ services:
|
|||||||
|
|
||||||
redis:
|
redis:
|
||||||
image: redis:5
|
image: redis:5
|
||||||
|
ports:
|
||||||
|
- "6379:6379"
|
||||||
|
|
||||||
mailcatcher:
|
mailcatcher:
|
||||||
image: sj26/mailcatcher:latest
|
image: sj26/mailcatcher:latest
|
||||||
@@ -271,11 +273,6 @@ services:
|
|||||||
- ./src/agents:/app
|
- ./src/agents:/app
|
||||||
- /app/.venv
|
- /app/.venv
|
||||||
|
|
||||||
redis-summary:
|
|
||||||
image: redis:5
|
|
||||||
ports:
|
|
||||||
- "6379:6379"
|
|
||||||
|
|
||||||
app-summary-dev:
|
app-summary-dev:
|
||||||
build:
|
build:
|
||||||
context: src/summary
|
context: src/summary
|
||||||
@@ -290,7 +287,7 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- ./src/summary:/app
|
- ./src/summary:/app
|
||||||
depends_on:
|
depends_on:
|
||||||
- redis-summary
|
- redis
|
||||||
|
|
||||||
celery-summary-transcribe:
|
celery-summary-transcribe:
|
||||||
container_name: celery-summary-transcribe
|
container_name: celery-summary-transcribe
|
||||||
@@ -304,7 +301,7 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- ./src/summary:/app
|
- ./src/summary:/app
|
||||||
depends_on:
|
depends_on:
|
||||||
- redis-summary
|
- redis
|
||||||
- app-summary-dev
|
- app-summary-dev
|
||||||
- garage
|
- garage
|
||||||
develop:
|
develop:
|
||||||
@@ -324,7 +321,7 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- ./src/summary:/app
|
- ./src/summary:/app
|
||||||
depends_on:
|
depends_on:
|
||||||
- redis-summary
|
- redis
|
||||||
- app-summary-dev
|
- app-summary-dev
|
||||||
- garage
|
- garage
|
||||||
develop:
|
develop:
|
||||||
|
|||||||
@@ -107,6 +107,37 @@ $ npm i
|
|||||||
$ npm run dev
|
$ npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### LiveKit agents (optional)
|
||||||
|
|
||||||
|
The LiveKit agents are not started by `make run`. Each one runs its own
|
||||||
|
container and stays connected to LiveKit, which costs CPU and memory you
|
||||||
|
don't need unless you work on the features they power. Start them only
|
||||||
|
when you need them.
|
||||||
|
|
||||||
|
| Agent | Feature | Make command | Setting in `env.d/development/common` |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `metadata-collector-dev` | Recording metadata (used to identify speakers in transcripts) | `make run-agent-metadata-collector` | `METADATA_COLLECTOR_ENABLED=True` |
|
||||||
|
| `multi-user-transcriber-dev` | Live subtitles | `make run-agent-multi-user-transcriber` | `ROOM_SUBTITLE_ENABLED=True` |
|
||||||
|
|
||||||
|
To start both at once:
|
||||||
|
```shellscript
|
||||||
|
$ make run-agents
|
||||||
|
```
|
||||||
|
|
||||||
|
Then set the matching settings to `True` and restart the backend so it
|
||||||
|
picks them up:
|
||||||
|
```shellscript
|
||||||
|
$ make run-backend
|
||||||
|
```
|
||||||
|
|
||||||
|
The multi-user transcriber also needs a speech-to-text provider. Configure
|
||||||
|
`STT_PROVIDER` and its credentials in
|
||||||
|
`env.d/development/multi_user_transcriber`.
|
||||||
|
|
||||||
|
Keep the settings and the agents in sync: if a setting is `True` while its
|
||||||
|
agent is stopped, the backend still dispatches jobs to it and the feature
|
||||||
|
fails silently.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Adding Content
|
## Adding Content
|
||||||
|
|||||||
@@ -104,10 +104,11 @@ ROOM_TELEPHONY_ENABLED=True
|
|||||||
# ROOMKIT_ENABLED = True
|
# ROOMKIT_ENABLED = True
|
||||||
# ROOMKIT_SERVER_TO_SERVER_API_TOKEN = ThisIsAnExampleKeyForDevPurposeOnly
|
# ROOMKIT_SERVER_TO_SERVER_API_TOKEN = ThisIsAnExampleKeyForDevPurposeOnly
|
||||||
|
|
||||||
# Metadata
|
# LiveKit agents (opt-in, start them with `make run-agents`)
|
||||||
METADATA_COLLECTOR_ENABLED=True
|
# Metadata (requires the metadata-collector agent)
|
||||||
|
METADATA_COLLECTOR_ENABLED=False
|
||||||
|
|
||||||
# Subtitle
|
# Subtitle (requires the multi-user-transcriber agent)
|
||||||
ROOM_SUBTITLE_ENABLED=False
|
ROOM_SUBTITLE_ENABLED=False
|
||||||
|
|
||||||
FRONTEND_USE_FRENCH_GOV_FOOTER=False
|
FRONTEND_USE_FRENCH_GOV_FOOTER=False
|
||||||
|
|||||||
@@ -9,6 +9,10 @@ AWS_S3_ACCESS_KEY_ID="meet-access-key"
|
|||||||
AWS_S3_SECRET_ACCESS_KEY="meet-secret-access-key"
|
AWS_S3_SECRET_ACCESS_KEY="meet-secret-access-key"
|
||||||
AWS_S3_REGION_NAME="local"
|
AWS_S3_REGION_NAME="local"
|
||||||
|
|
||||||
|
CELERY_BROKER_URL="redis://redis:6379/2"
|
||||||
|
CELERY_RESULT_BACKEND="redis://redis:6379/2"
|
||||||
|
TASK_TRACKER_REDIS_URL="redis://redis:6379/2"
|
||||||
|
|
||||||
WHISPERX_BASE_URL="https://configure-your-url.com"
|
WHISPERX_BASE_URL="https://configure-your-url.com"
|
||||||
WHISPERX_ASR_MODEL="large-v2"
|
WHISPERX_ASR_MODEL="large-v2"
|
||||||
WHISPERX_API_KEY="your-secret-key"
|
WHISPERX_API_KEY="your-secret-key"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "agents"
|
name = "agents"
|
||||||
version = "1.33.0"
|
version = "1.34.0"
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"livekit-agents==1.7.0",
|
"livekit-agents==1.7.0",
|
||||||
|
|||||||
Generated
+1
-1
@@ -9,7 +9,7 @@ resolution-markers = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "agents"
|
name = "agents"
|
||||||
version = "1.33.0"
|
version = "1.34.0"
|
||||||
source = { virtual = "." }
|
source = { virtual = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "boto3" },
|
{ name = "boto3" },
|
||||||
|
|||||||
@@ -4,51 +4,21 @@
|
|||||||
# ruff: noqa: PLR0913
|
# ruff: noqa: PLR0913
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from dataclasses import asdict
|
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.core.exceptions import SuspiciousOperation
|
from django.core.exceptions import SuspiciousOperation
|
||||||
|
|
||||||
import requests
|
|
||||||
from lasuite.oidc_resource_server.backend import ResourceServerBackend as LaSuiteBackend
|
from lasuite.oidc_resource_server.backend import ResourceServerBackend as LaSuiteBackend
|
||||||
from menshen_client import Configuration, IntrospectionRequest, TokenExchangeClient
|
|
||||||
from menshen_client.exceptions import ResponseParsingError
|
|
||||||
from rest_framework import authentication, exceptions
|
from rest_framework import authentication, exceptions
|
||||||
|
|
||||||
from core.models import Application, ApplicationScope
|
from core.models import Application
|
||||||
from core.services import jwt_token
|
from core.services import jwt_token
|
||||||
|
|
||||||
User = get_user_model()
|
User = get_user_model()
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def get_bearer_token(request):
|
|
||||||
"""Extract the bearer token from the Authorization header.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Token string, or None if the request carries no bearer token
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
AuthenticationFailed: If the Authorization header is malformed
|
|
||||||
"""
|
|
||||||
|
|
||||||
auth_header = authentication.get_authorization_header(request).split()
|
|
||||||
|
|
||||||
if not auth_header or auth_header[0].lower() != b"bearer":
|
|
||||||
return None
|
|
||||||
|
|
||||||
if len(auth_header) != 2:
|
|
||||||
logger.warning("Invalid token header format")
|
|
||||||
raise exceptions.AuthenticationFailed("Invalid token header.")
|
|
||||||
|
|
||||||
try:
|
|
||||||
return auth_header[1].decode("utf-8")
|
|
||||||
except UnicodeError as e:
|
|
||||||
logger.warning("Token decode error: %s", e)
|
|
||||||
raise exceptions.AuthenticationFailed("Invalid token encoding.") from e
|
|
||||||
|
|
||||||
|
|
||||||
class BaseJWTAuthentication(authentication.BaseAuthentication):
|
class BaseJWTAuthentication(authentication.BaseAuthentication):
|
||||||
"""Base JWT authentication class."""
|
"""Base JWT authentication class."""
|
||||||
|
|
||||||
@@ -101,12 +71,22 @@ class BaseJWTAuthentication(authentication.BaseAuthentication):
|
|||||||
if not self.is_enabled:
|
if not self.is_enabled:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
token = get_bearer_token(request)
|
auth_header = authentication.get_authorization_header(request).split()
|
||||||
|
|
||||||
if token is None:
|
if not auth_header or auth_header[0].lower() != b"bearer":
|
||||||
# Defer to next authentication backend
|
# Defer to next authentication backend
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
if len(auth_header) != 2:
|
||||||
|
logger.warning("Invalid token header format")
|
||||||
|
raise exceptions.AuthenticationFailed("Invalid token header.")
|
||||||
|
|
||||||
|
try:
|
||||||
|
token = auth_header[1].decode("utf-8")
|
||||||
|
except UnicodeError as e:
|
||||||
|
logger.warning("Token decode error: %s", e)
|
||||||
|
raise exceptions.AuthenticationFailed("Invalid token encoding.") from e
|
||||||
|
|
||||||
return self.authenticate_credentials(token)
|
return self.authenticate_credentials(token)
|
||||||
|
|
||||||
def decode_jwt(self, token):
|
def decode_jwt(self, token):
|
||||||
@@ -272,139 +252,6 @@ class AddonsJWTAuthentication(BaseJWTAuthentication):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# Menshen grants scopes following La Suite's "service:resource:action" convention.
|
|
||||||
# Map them to the scopes expected by the external API permissions.
|
|
||||||
MENSHEN_SCOPES_MAPPING = {
|
|
||||||
"meet:room:create": ApplicationScope.ROOMS_CREATE,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class MenshenAuthentication(authentication.BaseAuthentication):
|
|
||||||
"""Authentication for tokens exchanged through Menshen.
|
|
||||||
|
|
||||||
Menshen is La Suite's OAuth 2.0 token exchange server (RFC 8693): another service
|
|
||||||
exchanges its user's access token for a token targeting Meet, then calls the external
|
|
||||||
API on behalf of that user. Tokens are validated by introspection (RFC 7662). Menshen
|
|
||||||
only reports a token as active when Meet is among its audiences.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
"""Initialize the Menshen client from Django settings."""
|
|
||||||
|
|
||||||
super().__init__()
|
|
||||||
|
|
||||||
self._client = None
|
|
||||||
|
|
||||||
if not settings.MENSHEN_ENABLED:
|
|
||||||
return
|
|
||||||
|
|
||||||
self._client = TokenExchangeClient(
|
|
||||||
config=Configuration(
|
|
||||||
client_id=settings.MENSHEN_CLIENT_ID,
|
|
||||||
client_secret=settings.MENSHEN_CLIENT_SECRET,
|
|
||||||
server_root_url=settings.MENSHEN_SERVER_URL,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
def authenticate(self, request):
|
|
||||||
"""Introspect the bearer token with Menshen.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Tuple of (user, payload) if the token is active, None otherwise
|
|
||||||
"""
|
|
||||||
|
|
||||||
if self._client is None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
token = get_bearer_token(request)
|
|
||||||
|
|
||||||
if token is None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
payload = self.introspect(token)
|
|
||||||
|
|
||||||
if not payload.get("active"):
|
|
||||||
# Not a Menshen token, or an expired or revoked one: defer to next
|
|
||||||
# authentication backend
|
|
||||||
return None
|
|
||||||
|
|
||||||
user = self.get_user(payload)
|
|
||||||
|
|
||||||
scopes = payload.get("scope") or ""
|
|
||||||
payload["scope"] = [
|
|
||||||
MENSHEN_SCOPES_MAPPING[scope]
|
|
||||||
for scope in scopes.split()
|
|
||||||
if scope in MENSHEN_SCOPES_MAPPING
|
|
||||||
]
|
|
||||||
|
|
||||||
return (user, payload)
|
|
||||||
|
|
||||||
def introspect(self, token):
|
|
||||||
"""Submit the token to Menshen's introspection endpoint.
|
|
||||||
|
|
||||||
Errors are reported as an inactive token, so a Menshen outage doesn't
|
|
||||||
prevent the next authentication backends from running.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
token: Bearer token string
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Introspection response dict
|
|
||||||
"""
|
|
||||||
|
|
||||||
try:
|
|
||||||
response = self._client.introspect(IntrospectionRequest(token=token))
|
|
||||||
except (requests.RequestException, ResponseParsingError) as e:
|
|
||||||
logger.warning("Menshen introspection failed: %s", e)
|
|
||||||
return {"active": False}
|
|
||||||
|
|
||||||
# Permission classes expect a dict payload in request.auth
|
|
||||||
return asdict(response)
|
|
||||||
|
|
||||||
def get_user(self, payload):
|
|
||||||
"""Retrieve or create the user from the introspection response.
|
|
||||||
|
|
||||||
Menshen forwards the `sub` and `email` of the subject token, as introspected
|
|
||||||
with the OIDC provider.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
payload: Introspection response dict
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
User instance
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
AuthenticationFailed: If user not found or inactive
|
|
||||||
"""
|
|
||||||
|
|
||||||
sub = payload.get("sub")
|
|
||||||
|
|
||||||
if not sub:
|
|
||||||
logger.warning("Missing 'sub' in Menshen introspection response")
|
|
||||||
raise exceptions.AuthenticationFailed("Invalid token claims.")
|
|
||||||
|
|
||||||
try:
|
|
||||||
user = User.objects.get(sub=sub)
|
|
||||||
except User.DoesNotExist as e:
|
|
||||||
if not settings.OIDC_CREATE_USER:
|
|
||||||
logger.warning("User not found: %s", sub)
|
|
||||||
raise exceptions.AuthenticationFailed("User not found.") from e
|
|
||||||
|
|
||||||
user = User(sub=sub, email=payload.get("email"))
|
|
||||||
user.set_unusable_password()
|
|
||||||
user.save()
|
|
||||||
|
|
||||||
if not user.is_active:
|
|
||||||
logger.warning("Inactive user attempted authentication: %s", user.pk)
|
|
||||||
raise exceptions.AuthenticationFailed("User account is disabled.")
|
|
||||||
|
|
||||||
return user
|
|
||||||
|
|
||||||
def authenticate_header(self, request):
|
|
||||||
"""Return authentication scheme for WWW-Authenticate header."""
|
|
||||||
return "Bearer"
|
|
||||||
|
|
||||||
|
|
||||||
class ResourceServerBackend(LaSuiteBackend):
|
class ResourceServerBackend(LaSuiteBackend):
|
||||||
"""OIDC Resource Server backend for user creation and retrieval."""
|
"""OIDC Resource Server backend for user creation and retrieval."""
|
||||||
|
|
||||||
|
|||||||
@@ -166,7 +166,6 @@ class RoomViewSet(
|
|||||||
authentication_classes = [
|
authentication_classes = [
|
||||||
authentication.ApplicationJWTAuthentication,
|
authentication.ApplicationJWTAuthentication,
|
||||||
authentication.AddonsJWTAuthentication,
|
authentication.AddonsJWTAuthentication,
|
||||||
authentication.MenshenAuthentication,
|
|
||||||
ResourceServerAuthentication,
|
ResourceServerAuthentication,
|
||||||
]
|
]
|
||||||
permission_classes = [
|
permission_classes = [
|
||||||
|
|||||||
@@ -1,209 +0,0 @@
|
|||||||
"""Tests for the external API MenshenAuthentication."""
|
|
||||||
|
|
||||||
from unittest import mock
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
import responses
|
|
||||||
from lasuite.oidc_resource_server.authentication import ResourceServerAuthentication
|
|
||||||
from rest_framework.test import APIClient
|
|
||||||
|
|
||||||
from core.factories import UserFactory
|
|
||||||
from core.models import RoleChoices, Room, User
|
|
||||||
|
|
||||||
pytestmark = pytest.mark.django_db
|
|
||||||
|
|
||||||
SERVER_URL = "https://menshen.example.com"
|
|
||||||
INTROSPECTION_ENDPOINT = f"{SERVER_URL}/auth/token/introspect/"
|
|
||||||
TOKEN = "menshen-exchanged-token"
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
|
||||||
def menshen_settings(settings):
|
|
||||||
"""Enable Menshen authentication."""
|
|
||||||
settings.MENSHEN_ENABLED = True
|
|
||||||
settings.MENSHEN_SERVER_URL = SERVER_URL
|
|
||||||
settings.MENSHEN_CLIENT_ID = "meet"
|
|
||||||
settings.MENSHEN_CLIENT_SECRET = "meet-secret"
|
|
||||||
|
|
||||||
|
|
||||||
def _introspection(sub, scope="meet:room:create", **overrides):
|
|
||||||
return {
|
|
||||||
"active": True,
|
|
||||||
"sub": sub,
|
|
||||||
"email": "user@example.com",
|
|
||||||
"scope": scope,
|
|
||||||
"aud": "meet",
|
|
||||||
"client_id": "menshen",
|
|
||||||
**overrides,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def _create_room():
|
|
||||||
client = APIClient()
|
|
||||||
client.credentials(HTTP_AUTHORIZATION=f"Bearer {TOKEN}")
|
|
||||||
return client.post("/external-api/v1.0/rooms/", {}, format="json")
|
|
||||||
|
|
||||||
|
|
||||||
@responses.activate
|
|
||||||
def test_menshen_active_token():
|
|
||||||
"""An active token with a mapped scope should create a room owned by the user."""
|
|
||||||
|
|
||||||
user = UserFactory()
|
|
||||||
|
|
||||||
# Catch and mock external HTTP requests
|
|
||||||
responses.add(
|
|
||||||
responses.POST,
|
|
||||||
INTROSPECTION_ENDPOINT,
|
|
||||||
json=_introspection(user.sub),
|
|
||||||
match=[responses.matchers.urlencoded_params_matcher({"token": TOKEN})],
|
|
||||||
)
|
|
||||||
|
|
||||||
response = _create_room()
|
|
||||||
|
|
||||||
assert response.status_code == 201
|
|
||||||
room = Room.objects.get(id=response.data["id"])
|
|
||||||
assert room.get_role(user) == RoleChoices.OWNER
|
|
||||||
assert responses.calls[0].request.headers["Authorization"].startswith("Basic ")
|
|
||||||
|
|
||||||
|
|
||||||
@responses.activate
|
|
||||||
@mock.patch.object(ResourceServerAuthentication, "authenticate", return_value=None)
|
|
||||||
def test_menshen_disabled(mock_rs_authenticate, settings):
|
|
||||||
"""Menshen should not be called when disabled."""
|
|
||||||
|
|
||||||
settings.MENSHEN_ENABLED = False
|
|
||||||
|
|
||||||
response = _create_room()
|
|
||||||
|
|
||||||
assert response.status_code == 401
|
|
||||||
assert len(responses.calls) == 0
|
|
||||||
mock_rs_authenticate.assert_called_once()
|
|
||||||
|
|
||||||
|
|
||||||
@responses.activate
|
|
||||||
@mock.patch.object(ResourceServerAuthentication, "authenticate", return_value=None)
|
|
||||||
def test_menshen_inactive_token_defers(mock_rs_authenticate):
|
|
||||||
"""An inactive token should defer to the next authentication backend."""
|
|
||||||
|
|
||||||
user = UserFactory()
|
|
||||||
|
|
||||||
responses.add(
|
|
||||||
responses.POST,
|
|
||||||
INTROSPECTION_ENDPOINT,
|
|
||||||
json=_introspection(user.sub, scope="meet:room:create", active=False),
|
|
||||||
)
|
|
||||||
|
|
||||||
response = _create_room()
|
|
||||||
|
|
||||||
assert response.status_code == 401
|
|
||||||
mock_rs_authenticate.assert_called_once()
|
|
||||||
|
|
||||||
|
|
||||||
@responses.activate
|
|
||||||
@mock.patch.object(ResourceServerAuthentication, "authenticate", return_value=None)
|
|
||||||
def test_menshen_error_defers(mock_rs_authenticate):
|
|
||||||
"""A Menshen error should defer to the next authentication backend."""
|
|
||||||
|
|
||||||
responses.add(responses.POST, INTROSPECTION_ENDPOINT, status=500)
|
|
||||||
|
|
||||||
response = _create_room()
|
|
||||||
|
|
||||||
assert response.status_code == 401
|
|
||||||
mock_rs_authenticate.assert_called_once()
|
|
||||||
|
|
||||||
|
|
||||||
@responses.activate
|
|
||||||
@mock.patch.object(ResourceServerAuthentication, "authenticate", return_value=None)
|
|
||||||
def test_menshen_unparsable_response_defers(mock_rs_authenticate):
|
|
||||||
"""A response the client can't parse should defer to the next backend."""
|
|
||||||
|
|
||||||
responses.add(
|
|
||||||
responses.POST,
|
|
||||||
INTROSPECTION_ENDPOINT,
|
|
||||||
json={"active": True, "unexpected": "field"},
|
|
||||||
)
|
|
||||||
|
|
||||||
response = _create_room()
|
|
||||||
|
|
||||||
assert response.status_code == 401
|
|
||||||
mock_rs_authenticate.assert_called_once()
|
|
||||||
|
|
||||||
|
|
||||||
@responses.activate
|
|
||||||
def test_menshen_unmapped_scope():
|
|
||||||
"""Scopes granted for other services or other Meet actions should not grant access."""
|
|
||||||
|
|
||||||
user = UserFactory()
|
|
||||||
|
|
||||||
responses.add(
|
|
||||||
responses.POST,
|
|
||||||
INTROSPECTION_ENDPOINT,
|
|
||||||
json=_introspection(user.sub, scope="drive:item:create meet:room:list"),
|
|
||||||
)
|
|
||||||
|
|
||||||
response = _create_room()
|
|
||||||
|
|
||||||
assert response.status_code == 403
|
|
||||||
assert "insufficient permissions." in str(response.data).lower()
|
|
||||||
assert not Room.objects.exists()
|
|
||||||
|
|
||||||
|
|
||||||
@responses.activate
|
|
||||||
def test_menshen_missing_sub():
|
|
||||||
"""An active token without sub should be rejected."""
|
|
||||||
|
|
||||||
responses.add(responses.POST, INTROSPECTION_ENDPOINT, json=_introspection(None))
|
|
||||||
|
|
||||||
response = _create_room()
|
|
||||||
|
|
||||||
assert response.status_code == 401
|
|
||||||
assert "invalid token claims." in str(response.data).lower()
|
|
||||||
|
|
||||||
|
|
||||||
@responses.activate
|
|
||||||
def test_menshen_inactive_user():
|
|
||||||
"""An active token for an inactive user should be rejected."""
|
|
||||||
|
|
||||||
user = UserFactory(is_active=False)
|
|
||||||
|
|
||||||
responses.add(responses.POST, INTROSPECTION_ENDPOINT, json=_introspection(user.sub))
|
|
||||||
|
|
||||||
response = _create_room()
|
|
||||||
|
|
||||||
assert response.status_code == 401
|
|
||||||
assert "user account is disabled." in str(response.data).lower()
|
|
||||||
|
|
||||||
|
|
||||||
@responses.activate
|
|
||||||
def test_menshen_unknown_user_created(settings):
|
|
||||||
"""An unknown sub should create a user when OIDC_CREATE_USER is set."""
|
|
||||||
|
|
||||||
settings.OIDC_CREATE_USER = True
|
|
||||||
|
|
||||||
responses.add(
|
|
||||||
responses.POST, INTROSPECTION_ENDPOINT, json=_introspection("new-sub")
|
|
||||||
)
|
|
||||||
|
|
||||||
response = _create_room()
|
|
||||||
|
|
||||||
assert response.status_code == 201
|
|
||||||
user = User.objects.get(sub="new-sub")
|
|
||||||
assert user.email == "user@example.com"
|
|
||||||
assert user.is_active is True
|
|
||||||
|
|
||||||
|
|
||||||
@responses.activate
|
|
||||||
def test_menshen_unknown_user_not_created(settings):
|
|
||||||
"""An unknown sub should be rejected when OIDC_CREATE_USER is unset."""
|
|
||||||
|
|
||||||
settings.OIDC_CREATE_USER = False
|
|
||||||
|
|
||||||
responses.add(
|
|
||||||
responses.POST, INTROSPECTION_ENDPOINT, json=_introspection("new-sub")
|
|
||||||
)
|
|
||||||
|
|
||||||
response = _create_room()
|
|
||||||
|
|
||||||
assert response.status_code == 401
|
|
||||||
assert "user not found." in str(response.data).lower()
|
|
||||||
assert not User.objects.filter(sub="new-sub").exists()
|
|
||||||
@@ -702,20 +702,6 @@ class Base(Configuration):
|
|||||||
default=None, environ_name="OIDC_RS_SCOPES_PREFIX", environ_prefix=None
|
default=None, environ_name="OIDC_RS_SCOPES_PREFIX", environ_prefix=None
|
||||||
)
|
)
|
||||||
|
|
||||||
# Menshen, La Suite's OAuth 2.0 token exchange server
|
|
||||||
MENSHEN_ENABLED = values.BooleanValue(
|
|
||||||
False, environ_name="MENSHEN_ENABLED", environ_prefix=None
|
|
||||||
)
|
|
||||||
MENSHEN_SERVER_URL = values.Value(
|
|
||||||
None, environ_name="MENSHEN_SERVER_URL", environ_prefix=None
|
|
||||||
)
|
|
||||||
MENSHEN_CLIENT_ID = values.Value(
|
|
||||||
None, environ_name="MENSHEN_CLIENT_ID", environ_prefix=None
|
|
||||||
)
|
|
||||||
MENSHEN_CLIENT_SECRET = SecretFileValue(
|
|
||||||
None, environ_name="MENSHEN_CLIENT_SECRET", environ_prefix=None
|
|
||||||
)
|
|
||||||
|
|
||||||
# Video conference configuration
|
# Video conference configuration
|
||||||
LIVEKIT_CONFIGURATION = {
|
LIVEKIT_CONFIGURATION = {
|
||||||
"api_key": SecretFileValue(environ_name="LIVEKIT_API_KEY", environ_prefix=None),
|
"api_key": SecretFileValue(environ_name="LIVEKIT_API_KEY", environ_prefix=None),
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ build-backend = "uv_build"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "meet"
|
name = "meet"
|
||||||
version = "1.33.0"
|
version = "1.34.0"
|
||||||
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
|
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
|
||||||
classifiers = [
|
classifiers = [
|
||||||
"Development Status :: 5 - Production/Stable",
|
"Development Status :: 5 - Production/Stable",
|
||||||
@@ -49,7 +49,6 @@ dependencies = [
|
|||||||
"gunicorn==26.2.0",
|
"gunicorn==26.2.0",
|
||||||
"jsonschema==4.26.0",
|
"jsonschema==4.26.0",
|
||||||
"markdown==3.10.3",
|
"markdown==3.10.3",
|
||||||
"menshen-client==0.1.0",
|
|
||||||
"nested-multipart-parser==1.6.0",
|
"nested-multipart-parser==1.6.0",
|
||||||
"posthog==7.44.0",
|
"posthog==7.44.0",
|
||||||
"psycopg[binary]==3.3.4",
|
"psycopg[binary]==3.3.4",
|
||||||
|
|||||||
Generated
+1
-15
@@ -1297,7 +1297,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "meet"
|
name = "meet"
|
||||||
version = "1.33.0"
|
version = "1.34.0"
|
||||||
source = { editable = "." }
|
source = { editable = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "aiohttp" },
|
{ name = "aiohttp" },
|
||||||
@@ -1327,7 +1327,6 @@ dependencies = [
|
|||||||
{ name = "jsonschema" },
|
{ name = "jsonschema" },
|
||||||
{ name = "livekit-api" },
|
{ name = "livekit-api" },
|
||||||
{ name = "markdown" },
|
{ name = "markdown" },
|
||||||
{ name = "menshen-client" },
|
|
||||||
{ name = "mozilla-django-oidc" },
|
{ name = "mozilla-django-oidc" },
|
||||||
{ name = "nested-multipart-parser" },
|
{ name = "nested-multipart-parser" },
|
||||||
{ name = "phonenumbers" },
|
{ name = "phonenumbers" },
|
||||||
@@ -1393,7 +1392,6 @@ requires-dist = [
|
|||||||
{ name = "jsonschema", specifier = "==4.26.0" },
|
{ name = "jsonschema", specifier = "==4.26.0" },
|
||||||
{ name = "livekit-api", specifier = "==1.2.0" },
|
{ name = "livekit-api", specifier = "==1.2.0" },
|
||||||
{ name = "markdown", specifier = "==3.10.3" },
|
{ name = "markdown", specifier = "==3.10.3" },
|
||||||
{ name = "menshen-client", specifier = "==0.1.0" },
|
|
||||||
{ name = "mozilla-django-oidc", specifier = "==5.0.2" },
|
{ name = "mozilla-django-oidc", specifier = "==5.0.2" },
|
||||||
{ name = "nested-multipart-parser", specifier = "==1.6.0" },
|
{ name = "nested-multipart-parser", specifier = "==1.6.0" },
|
||||||
{ name = "phonenumbers", specifier = "==9.0.37" },
|
{ name = "phonenumbers", specifier = "==9.0.37" },
|
||||||
@@ -1430,18 +1428,6 @@ dev = [
|
|||||||
{ name = "types-requests", specifier = "==2.33.0.20260712" },
|
{ name = "types-requests", specifier = "==2.33.0.20260712" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "menshen-client"
|
|
||||||
version = "0.1.0"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "requests" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/53/d5/c2be400c190efe26b70e5c60dd9d9ab279ece3b51de457abc184a238c727/menshen_client-0.1.0.tar.gz", hash = "sha256:21fe48788e860c7f2e103787ce0827981e9059146cf7bdb9ac1a8778acb9ff77", size = 6219, upload-time = "2026-09-21T21:04:01.677Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d6/bf/11d15da4852b8344eb9f30861bd15a8f1d931ff4232d9b7de18fce09fd3c/menshen_client-0.1.0-py3-none-any.whl", hash = "sha256:d0dfb351812bb93620a4796e0f0ce1a40e8ec859dc8de2a1f50ae7f306d4d29a", size = 7040, upload-time = "2026-09-21T21:04:00.433Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mozilla-django-oidc"
|
name = "mozilla-django-oidc"
|
||||||
version = "5.0.2"
|
version = "5.0.2"
|
||||||
|
|||||||
Generated
+280
-6
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "meet",
|
"name": "meet",
|
||||||
"version": "1.33.0",
|
"version": "1.34.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "meet",
|
"name": "meet",
|
||||||
"version": "1.33.0",
|
"version": "1.34.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fontsource-variable/atkinson-hyperlegible-next": "5.3.0",
|
"@fontsource-variable/atkinson-hyperlegible-next": "5.3.0",
|
||||||
"@fontsource-variable/lexend": "5.3.0",
|
"@fontsource-variable/lexend": "5.3.0",
|
||||||
@@ -64,7 +64,8 @@
|
|||||||
"typescript-eslint": "8.60.1",
|
"typescript-eslint": "8.60.1",
|
||||||
"vite": "8.0.14",
|
"vite": "8.0.14",
|
||||||
"vite-plugin-static-copy": "4.1.1",
|
"vite-plugin-static-copy": "4.1.1",
|
||||||
"vite-plugin-svgr": "5.2.0"
|
"vite-plugin-svgr": "5.2.0",
|
||||||
|
"vitest": "5.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@adobe/react-spectrum": {
|
"node_modules/@adobe/react-spectrum": {
|
||||||
@@ -942,9 +943,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@jridgewell/sourcemap-codec": {
|
"node_modules/@jridgewell/sourcemap-codec": {
|
||||||
"version": "1.5.5",
|
"version": "1.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz",
|
||||||
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
|
"integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
@@ -2506,6 +2507,24 @@
|
|||||||
"tslib": "^2.4.0"
|
"tslib": "^2.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/chai": {
|
||||||
|
"version": "5.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
|
||||||
|
"integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/deep-eql": "*",
|
||||||
|
"assertion-error": "^2.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/deep-eql": {
|
||||||
|
"version": "4.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
|
||||||
|
"integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/dom-mediacapture-record": {
|
"node_modules/@types/dom-mediacapture-record": {
|
||||||
"version": "1.0.22",
|
"version": "1.0.22",
|
||||||
"resolved": "https://registry.npmjs.org/@types/dom-mediacapture-record/-/dom-mediacapture-record-1.0.22.tgz",
|
"resolved": "https://registry.npmjs.org/@types/dom-mediacapture-record/-/dom-mediacapture-record-1.0.22.tgz",
|
||||||
@@ -2786,6 +2805,64 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@vitest/mocker": {
|
||||||
|
"version": "5.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-5.0.2.tgz",
|
||||||
|
"integrity": "sha512-Z5FS00Q1SJHkB35xATsmWGdQ5WA1/0MV3CDjqyv7GavHv1OfOj145MNfHOlHk7QLes21dKFDHr8EO2zvL+9WGA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@jridgewell/trace-mapping": "0.3.31",
|
||||||
|
"@vitest/spy": "5.0.2",
|
||||||
|
"estree-walker": "^3.0.3",
|
||||||
|
"magic-string": "^1.2.3"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/vitest"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"msw": "^2.4.9",
|
||||||
|
"vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"msw": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"vite": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vitest/mocker/node_modules/estree-walker": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/estree": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vitest/mocker/node_modules/magic-string": {
|
||||||
|
"version": "1.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.2.tgz",
|
||||||
|
"integrity": "sha512-vG+rjFRj1PqdIBozIxAGMjPlOhaVe+GXpbttY/iSK7rGcJRMlwNJO7dcUwmUqkymsFLJiNGI06t4D7Fr7yRC9g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@jridgewell/sourcemap-codec": "^1.6.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vitest/spy": {
|
||||||
|
"version": "5.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-5.0.2.tgz",
|
||||||
|
"integrity": "sha512-Ijc7T1nT9efNb5LxvjaBrEqw3f/QwUv5EE0nKqZxgqsaV/FxAAZ8baGylA8X/Z2oS4Lp+K74Jr6dTJsDKxJDeg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/vitest"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@vue/compiler-core": {
|
"node_modules/@vue/compiler-core": {
|
||||||
"version": "3.5.25",
|
"version": "3.5.25",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.25.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.25.tgz",
|
||||||
@@ -3151,6 +3228,16 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/assertion-error": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ast-types-flow": {
|
"node_modules/ast-types-flow": {
|
||||||
"version": "0.0.8",
|
"version": "0.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz",
|
||||||
@@ -4130,6 +4217,16 @@
|
|||||||
],
|
],
|
||||||
"license": "CC-BY-4.0"
|
"license": "CC-BY-4.0"
|
||||||
},
|
},
|
||||||
|
"node_modules/chai": {
|
||||||
|
"version": "6.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz",
|
||||||
|
"integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/chalk": {
|
"node_modules/chalk": {
|
||||||
"version": "4.1.2",
|
"version": "4.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||||
@@ -5030,6 +5127,13 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/es-module-lexer": {
|
||||||
|
"version": "2.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz",
|
||||||
|
"integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/es-object-atoms": {
|
"node_modules/es-object-atoms": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
||||||
@@ -5532,6 +5636,16 @@
|
|||||||
"node": ">=18.0.0"
|
"node": ">=18.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/expect-type": {
|
||||||
|
"version": "1.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz",
|
||||||
|
"integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/express": {
|
"node_modules/express": {
|
||||||
"version": "5.2.1",
|
"version": "5.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
|
||||||
@@ -8647,6 +8761,20 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/obug": {
|
||||||
|
"version": "2.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/obug/-/obug-2.2.1.tgz",
|
||||||
|
"integrity": "sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==",
|
||||||
|
"dev": true,
|
||||||
|
"funding": [
|
||||||
|
"https://github.com/sponsors/sxzz",
|
||||||
|
"https://opencollective.com/debug"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.20.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/on-finished": {
|
"node_modules/on-finished": {
|
||||||
"version": "2.4.1",
|
"version": "2.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
|
||||||
@@ -10220,6 +10348,13 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/std-env": {
|
||||||
|
"version": "4.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/std-env/-/std-env-4.3.0.tgz",
|
||||||
|
"integrity": "sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/stop-iteration-iterator": {
|
"node_modules/stop-iteration-iterator": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz",
|
||||||
@@ -10526,6 +10661,26 @@
|
|||||||
"xtend": "~4.0.1"
|
"xtend": "~4.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tinybench": {
|
||||||
|
"version": "6.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-6.2.0.tgz",
|
||||||
|
"integrity": "sha512-78U2TlB2CnVenajOFzf3BKSm0J6oz5L0NV7g32LCPccvYc0lbWvys4d3uUUCS2B1N8PAf2+aekR8i1KbC3HO7Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tinyexec": {
|
||||||
|
"version": "1.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.1.tgz",
|
||||||
|
"integrity": "sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/tinyglobby": {
|
"node_modules/tinyglobby": {
|
||||||
"version": "0.2.17",
|
"version": "0.2.17",
|
||||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
|
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
|
||||||
@@ -11610,6 +11765,112 @@
|
|||||||
"url": "https://github.com/sponsors/jonschlinkert"
|
"url": "https://github.com/sponsors/jonschlinkert"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/vitest": {
|
||||||
|
"version": "5.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/vitest/-/vitest-5.0.2.tgz",
|
||||||
|
"integrity": "sha512-7MQrx9pDv5aHiUcovIb/70Ys3tgtkUVgCtledvKdCmEO+/1Dicq5ZqoSxOW034m03oqC+oHOKui2dM6qtMLoJg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/chai": "^5.2.2",
|
||||||
|
"@vitest/mocker": "5.0.2",
|
||||||
|
"chai": "^6.2.2",
|
||||||
|
"es-module-lexer": "^2.3.2",
|
||||||
|
"expect-type": "^1.4.0",
|
||||||
|
"magic-string": "^1.2.3",
|
||||||
|
"obug": "^2.1.4",
|
||||||
|
"picomatch": "^4.0.7",
|
||||||
|
"std-env": "^4.2.0",
|
||||||
|
"tinybench": "^6.1.4",
|
||||||
|
"tinyexec": "^1.3.0",
|
||||||
|
"tinyglobby": "^0.2.17",
|
||||||
|
"why-is-node-running": "^3.2.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"vitest": "vitest.mjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^22.12.0 || ^24.0.0 || >=26.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/vitest"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@edge-runtime/vm": "*",
|
||||||
|
"@opentelemetry/api": "^1.9.0",
|
||||||
|
"@types/node": "^22.0.0 || >=24.0.0",
|
||||||
|
"@vitest/browser-playwright": "5.0.2",
|
||||||
|
"@vitest/browser-preview": "5.0.2",
|
||||||
|
"@vitest/browser-webdriverio": "^5.0.0-beta.5 || >=5.0.0",
|
||||||
|
"@vitest/coverage-istanbul": "5.0.2",
|
||||||
|
"@vitest/coverage-v8": "5.0.2",
|
||||||
|
"@vitest/ui": "5.0.2",
|
||||||
|
"happy-dom": "*",
|
||||||
|
"jsdom": "*",
|
||||||
|
"vite": "^6.4.0 || ^7.0.0 || ^8.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@edge-runtime/vm": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@opentelemetry/api": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/node": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@vitest/browser-playwright": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@vitest/browser-preview": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@vitest/browser-webdriverio": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@vitest/coverage-istanbul": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@vitest/coverage-v8": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@vitest/ui": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"happy-dom": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"jsdom": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"vite": {
|
||||||
|
"optional": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/vitest/node_modules/magic-string": {
|
||||||
|
"version": "1.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.2.tgz",
|
||||||
|
"integrity": "sha512-vG+rjFRj1PqdIBozIxAGMjPlOhaVe+GXpbttY/iSK7rGcJRMlwNJO7dcUwmUqkymsFLJiNGI06t4D7Fr7yRC9g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@jridgewell/sourcemap-codec": "^1.6.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/vitest/node_modules/picomatch": {
|
||||||
|
"version": "4.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz",
|
||||||
|
"integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/jonschlinkert"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/walk-sync": {
|
"node_modules/walk-sync": {
|
||||||
"version": "2.2.0",
|
"version": "2.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/walk-sync/-/walk-sync-2.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/walk-sync/-/walk-sync-2.2.0.tgz",
|
||||||
@@ -11794,6 +12055,19 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/why-is-node-running": {
|
||||||
|
"version": "3.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-3.2.2.tgz",
|
||||||
|
"integrity": "sha512-NKUzAelcoCXhXL4dJzKIwXeR8iEVqsA0Lq6Vnd0UXvgaKbzVo4ZTHROF2Jidrv+SgxOQ03fMinnNhzZATxOD3A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"why-is-node-running": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.11"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/word-wrap": {
|
"node_modules/word-wrap": {
|
||||||
"version": "1.2.5",
|
"version": "1.2.5",
|
||||||
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
{
|
{
|
||||||
"name": "meet",
|
"name": "meet",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.33.0",
|
"version": "1.34.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "panda codegen && vite",
|
"dev": "panda codegen && vite",
|
||||||
"build": "panda codegen && tsc -b && vite build",
|
"build": "panda codegen && tsc -b && vite build",
|
||||||
"build:debug": "VITE_ANALYZE=true npm run build -- --debug",
|
"build:debug": "VITE_ANALYZE=true npm run build -- --debug",
|
||||||
|
"test": "panda codegen && vitest run",
|
||||||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||||
"lint:fix": "eslint . --fix",
|
"lint:fix": "eslint . --fix",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
@@ -71,6 +72,7 @@
|
|||||||
"typescript-eslint": "8.60.1",
|
"typescript-eslint": "8.60.1",
|
||||||
"vite": "8.0.14",
|
"vite": "8.0.14",
|
||||||
"vite-plugin-static-copy": "4.1.1",
|
"vite-plugin-static-copy": "4.1.1",
|
||||||
"vite-plugin-svgr": "5.2.0"
|
"vite-plugin-svgr": "5.2.0",
|
||||||
|
"vitest": "5.0.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import {
|
|||||||
useSwipe,
|
useSwipe,
|
||||||
} from '@livekit/components-react'
|
} from '@livekit/components-react'
|
||||||
import { mergeProps } from '@/utils/mergeProps'
|
import { mergeProps } from '@/utils/mergeProps'
|
||||||
import { PaginationIndicator } from './PaginationIndicator'
|
|
||||||
import { useGridLayout } from '../hooks/useGridLayout'
|
import { useGridLayout } from '../hooks/useGridLayout'
|
||||||
import { PaginationControl } from './PaginationControl'
|
import { PaginationControl } from './PaginationControl'
|
||||||
import { useSpeakerPromotionTrigger } from '../hooks/useSpeakerPromotionTrigger'
|
import { useSpeakerPromotionTrigger } from '../hooks/useSpeakerPromotionTrigger'
|
||||||
@@ -62,13 +61,7 @@ export function GridLayout({ tracks, ...props }: GridLayoutProps) {
|
|||||||
>
|
>
|
||||||
<TrackLoop tracks={pagination.tracks}>{props.children}</TrackLoop>
|
<TrackLoop tracks={pagination.tracks}>{props.children}</TrackLoop>
|
||||||
{tracks.length > layout.maxTiles && (
|
{tracks.length > layout.maxTiles && (
|
||||||
<>
|
<PaginationControl {...pagination} focusShortcut />
|
||||||
<PaginationIndicator
|
|
||||||
totalPageCount={pagination.totalPageCount}
|
|
||||||
currentPage={pagination.currentPage}
|
|
||||||
/>
|
|
||||||
<PaginationControl pagesContainer={gridEl} {...pagination} />
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,99 +1,147 @@
|
|||||||
import * as React from 'react'
|
|
||||||
import { createInteractingObservable } from '@livekit/components-core'
|
|
||||||
import { RiArrowLeftSLine, RiArrowRightSLine } from '@remixicon/react'
|
import { RiArrowLeftSLine, RiArrowRightSLine } from '@remixicon/react'
|
||||||
import { Button } from '@/primitives'
|
import { Button } from '@/primitives'
|
||||||
import { useEffect, useState } from 'react'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { css } from '@/styled-system/css'
|
import { css, cva, type RecipeVariantProps } from '@/styled-system/css'
|
||||||
|
import { useCallback, useRef } from 'react'
|
||||||
|
import { useRegisterKeyboardShortcut } from '@/features/shortcuts/useRegisterKeyboardShortcut'
|
||||||
|
|
||||||
export interface PaginationControlProps {
|
const paginationToolbar = cva({
|
||||||
|
base: {
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '0.125rem',
|
||||||
|
backgroundColor: 'primaryDark.50',
|
||||||
|
borderRadius: '2rem',
|
||||||
|
border: '1px solid',
|
||||||
|
borderColor: 'primaryDark.200',
|
||||||
|
padding: '0.375rem',
|
||||||
|
},
|
||||||
|
variants: {
|
||||||
|
placement: {
|
||||||
|
overlay: {
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: '1rem',
|
||||||
|
left: '50%',
|
||||||
|
zIndex: 2,
|
||||||
|
transform: 'translateX(-50%)',
|
||||||
|
boxShadow: '0 2px 10px rgba(0, 0, 0, 0.45)',
|
||||||
|
},
|
||||||
|
inline: {
|
||||||
|
alignSelf: 'center',
|
||||||
|
flexShrink: 0,
|
||||||
|
marginTop: '1rem',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
placement: 'overlay',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export type PaginationControlProps = RecipeVariantProps<
|
||||||
|
typeof paginationToolbar
|
||||||
|
> & {
|
||||||
totalPageCount: number
|
totalPageCount: number
|
||||||
nextPage: () => void
|
nextPage: () => void
|
||||||
prevPage: () => void
|
prevPage: () => void
|
||||||
currentPage: number
|
currentPage: number
|
||||||
pagesContainer?: React.RefObject<HTMLElement>
|
// The shortcut listens on the main window only, so a single instance may own it.
|
||||||
|
focusShortcut?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const arrowButtonClass = css({
|
||||||
|
_disabled: {
|
||||||
|
cursor: 'default',
|
||||||
|
backgroundColor: 'transparent !important',
|
||||||
|
'& svg': {
|
||||||
|
opacity: 0.35,
|
||||||
|
},
|
||||||
|
_focusVisible: {
|
||||||
|
outline: '2px solid',
|
||||||
|
outlineColor: 'focusRing',
|
||||||
|
outlineOffset: '2px',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
export function PaginationControl({
|
export function PaginationControl({
|
||||||
totalPageCount,
|
totalPageCount,
|
||||||
nextPage,
|
nextPage,
|
||||||
prevPage,
|
prevPage,
|
||||||
currentPage,
|
currentPage,
|
||||||
pagesContainer: connectedElement,
|
placement,
|
||||||
|
focusShortcut = false,
|
||||||
}: PaginationControlProps) {
|
}: PaginationControlProps) {
|
||||||
const { t } = useTranslation('rooms', { keyPrefix: 'pagination' })
|
const { t } = useTranslation('rooms', { keyPrefix: 'pagination' })
|
||||||
const [interactive, setInteractive] = useState(false)
|
const prevButtonRef = useRef<HTMLButtonElement>(null)
|
||||||
|
const nextButtonRef = useRef<HTMLButtonElement>(null)
|
||||||
|
const isSinglePage = totalPageCount <= 1
|
||||||
|
const isFirstPage = currentPage <= 1
|
||||||
|
const isLastPage = currentPage >= totalPageCount
|
||||||
|
|
||||||
useEffect(() => {
|
const focusPagination = useCallback(() => {
|
||||||
let subscription:
|
const target = isLastPage ? prevButtonRef.current : nextButtonRef.current
|
||||||
| ReturnType<ReturnType<typeof createInteractingObservable>['subscribe']>
|
target?.focus()
|
||||||
| undefined
|
}, [isLastPage])
|
||||||
if (connectedElement) {
|
|
||||||
subscription = createInteractingObservable(
|
|
||||||
connectedElement.current,
|
|
||||||
2000
|
|
||||||
).subscribe(setInteractive)
|
|
||||||
}
|
|
||||||
return () => {
|
|
||||||
if (subscription) {
|
|
||||||
subscription.unsubscribe()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [connectedElement])
|
|
||||||
|
|
||||||
if (totalPageCount <= 1) return null
|
useRegisterKeyboardShortcut({
|
||||||
|
id: focusShortcut ? 'focus-pagination' : undefined,
|
||||||
|
handler: focusPagination,
|
||||||
|
isDisabled: isSinglePage,
|
||||||
|
unregisterOnUnmount: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (isSinglePage) return null
|
||||||
|
|
||||||
|
const pageCount = t('count', { currentPage, totalPageCount })
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav
|
<div
|
||||||
aria-label={t('label')}
|
role="group"
|
||||||
className={css({
|
aria-label={`${t('label')}, ${pageCount}`}
|
||||||
position: 'absolute',
|
className={paginationToolbar({ placement })}
|
||||||
bottom: '1rem',
|
|
||||||
left: '50%',
|
|
||||||
transform: 'translateX(-50%)',
|
|
||||||
alignItems: 'stretch',
|
|
||||||
backgroundColor: 'var(--lk-control-bg)',
|
|
||||||
borderRadius: 'var(--lk-border-radius)',
|
|
||||||
transition: 'opacity ease-in-out .15s',
|
|
||||||
display: 'none',
|
|
||||||
border: '1px solid',
|
|
||||||
borderColor: 'primaryDark.100',
|
|
||||||
overflow: 'hidden',
|
|
||||||
})}
|
|
||||||
style={{
|
|
||||||
display: interactive ? 'flex' : 'none',
|
|
||||||
}}
|
|
||||||
data-lk-user-interaction={interactive}
|
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
isDisabled={currentPage == 1}
|
ref={prevButtonRef}
|
||||||
onPress={prevPage}
|
aria-disabled={isFirstPage}
|
||||||
size="xs"
|
onPress={isFirstPage ? undefined : prevPage}
|
||||||
variant="quaternaryText"
|
size="sm"
|
||||||
|
square
|
||||||
|
variant="primaryTextDark"
|
||||||
|
className={arrowButtonClass}
|
||||||
aria-label={t('previous')}
|
aria-label={t('previous')}
|
||||||
|
tooltip={t('previous')}
|
||||||
>
|
>
|
||||||
<RiArrowLeftSLine />
|
<RiArrowLeftSLine size={20} />
|
||||||
</Button>
|
</Button>
|
||||||
<span
|
<span
|
||||||
role="status"
|
role="status"
|
||||||
className={css({
|
className={css({
|
||||||
padding: '0.25rem 0.5rem',
|
color: 'white',
|
||||||
|
fontSize: '0.8125rem',
|
||||||
|
fontWeight: 500,
|
||||||
|
minWidth: '3.5rem',
|
||||||
|
textAlign: 'center',
|
||||||
|
userSelect: 'none',
|
||||||
|
padding: '0 0.35rem',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{t('count', {
|
{pageCount}
|
||||||
currentPage,
|
|
||||||
totalPageCount,
|
|
||||||
})}
|
|
||||||
</span>
|
</span>
|
||||||
<Button
|
<Button
|
||||||
isDisabled={currentPage == totalPageCount}
|
ref={nextButtonRef}
|
||||||
onPress={nextPage}
|
aria-disabled={isLastPage}
|
||||||
size="xs"
|
onPress={isLastPage ? undefined : nextPage}
|
||||||
variant="quaternaryText"
|
size="sm"
|
||||||
|
square
|
||||||
|
variant="primaryTextDark"
|
||||||
|
className={arrowButtonClass}
|
||||||
aria-label={t('next')}
|
aria-label={t('next')}
|
||||||
|
tooltip={t('next')}
|
||||||
>
|
>
|
||||||
<RiArrowRightSLine />
|
<RiArrowRightSLine size={20} />
|
||||||
</Button>
|
</Button>
|
||||||
</nav>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
import * as React from 'react'
|
|
||||||
|
|
||||||
export interface PaginationIndicatorProps {
|
|
||||||
totalPageCount: number
|
|
||||||
currentPage: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export const PaginationIndicator: (
|
|
||||||
props: PaginationIndicatorProps & React.RefAttributes<HTMLDivElement>
|
|
||||||
) => React.ReactNode = /* @__PURE__ */ React.forwardRef<
|
|
||||||
HTMLDivElement,
|
|
||||||
PaginationIndicatorProps
|
|
||||||
>(function PaginationIndicator(
|
|
||||||
{ totalPageCount, currentPage }: PaginationIndicatorProps,
|
|
||||||
ref
|
|
||||||
) {
|
|
||||||
const bubbles = new Array(totalPageCount).fill('').map((_, index) => {
|
|
||||||
if (index + 1 === currentPage) {
|
|
||||||
return <span data-lk-active key={index} />
|
|
||||||
} else {
|
|
||||||
return <span key={index} />
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div ref={ref} className="lk-pagination-indicator" aria-hidden="true">
|
|
||||||
{bubbles}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
import { RiArrowLeftSLine, RiArrowRightSLine } from '@remixicon/react'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
import { styled } from '@/styled-system/jsx'
|
|
||||||
|
|
||||||
interface PipPaginationProps {
|
|
||||||
totalPageCount: number
|
|
||||||
currentPage: number
|
|
||||||
nextPage: () => void
|
|
||||||
prevPage: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export const PipPagination = ({
|
|
||||||
totalPageCount,
|
|
||||||
currentPage,
|
|
||||||
nextPage,
|
|
||||||
prevPage,
|
|
||||||
}: PipPaginationProps) => {
|
|
||||||
const { t } = useTranslation('rooms', { keyPrefix: 'pagination' })
|
|
||||||
|
|
||||||
if (totalPageCount <= 1) return null
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Nav aria-label={t('label')}>
|
|
||||||
<ArrowButton
|
|
||||||
type="button"
|
|
||||||
onClick={prevPage}
|
|
||||||
disabled={currentPage === 1}
|
|
||||||
aria-label={t('previous')}
|
|
||||||
>
|
|
||||||
<RiArrowLeftSLine size={18} />
|
|
||||||
</ArrowButton>
|
|
||||||
<Counter role="status">
|
|
||||||
{t('count', { currentPage, totalPageCount })}
|
|
||||||
</Counter>
|
|
||||||
<ArrowButton
|
|
||||||
type="button"
|
|
||||||
onClick={nextPage}
|
|
||||||
disabled={currentPage === totalPageCount}
|
|
||||||
aria-label={t('next')}
|
|
||||||
>
|
|
||||||
<RiArrowRightSLine size={18} />
|
|
||||||
</ArrowButton>
|
|
||||||
</Nav>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const Nav = styled('nav', {
|
|
||||||
base: {
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
gap: '0.25rem',
|
|
||||||
marginTop: '1rem',
|
|
||||||
flexShrink: 0,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const ArrowButton = styled('button', {
|
|
||||||
base: {
|
|
||||||
display: 'inline-flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
width: '1.75rem',
|
|
||||||
height: '1.75rem',
|
|
||||||
borderRadius: '4px',
|
|
||||||
border: 'none',
|
|
||||||
cursor: 'pointer',
|
|
||||||
color: 'white',
|
|
||||||
backgroundColor: 'primaryDark.100',
|
|
||||||
transition: 'opacity 0.15s, background-color 0.15s',
|
|
||||||
'&:hover:not(:disabled)': {
|
|
||||||
backgroundColor: 'primaryDark.75',
|
|
||||||
},
|
|
||||||
'&:focus-visible': {
|
|
||||||
outline: '2px solid',
|
|
||||||
outlineColor: 'white',
|
|
||||||
outlineOffset: '2px',
|
|
||||||
},
|
|
||||||
'&:disabled': {
|
|
||||||
opacity: 0.3,
|
|
||||||
cursor: 'default',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const Counter = styled('span', {
|
|
||||||
base: {
|
|
||||||
fontSize: '0.75rem',
|
|
||||||
color: 'white',
|
|
||||||
opacity: 0.8,
|
|
||||||
whiteSpace: 'nowrap',
|
|
||||||
padding: '0 0.25rem',
|
|
||||||
minWidth: '3rem',
|
|
||||||
textAlign: 'center',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
@@ -4,7 +4,7 @@ import { RoomEvent, Track } from 'livekit-client'
|
|||||||
import { styled } from '@/styled-system/jsx'
|
import { styled } from '@/styled-system/jsx'
|
||||||
import { PipFocusLayout } from './PipFocusLayout'
|
import { PipFocusLayout } from './PipFocusLayout'
|
||||||
import { PipGridLayout } from './PipGridLayout'
|
import { PipGridLayout } from './PipGridLayout'
|
||||||
import { PipPagination } from './PipPagination'
|
import { PaginationControl } from '@/features/layout/components/PaginationControl'
|
||||||
import { PipScreenShareLayout } from './PipScreenShareLayout'
|
import { PipScreenShareLayout } from './PipScreenShareLayout'
|
||||||
import { StageFrame } from './StageFrame'
|
import { StageFrame } from './StageFrame'
|
||||||
import { MAX_PIP_TILES } from '../../utils/pipGrid'
|
import { MAX_PIP_TILES } from '../../utils/pipGrid'
|
||||||
@@ -120,12 +120,7 @@ const PaginatedStage = ({
|
|||||||
}) => (
|
}) => (
|
||||||
<StageWrapper>
|
<StageWrapper>
|
||||||
<StageFrame>{children}</StageFrame>
|
<StageFrame>{children}</StageFrame>
|
||||||
<PipPagination
|
<PaginationControl {...pagination} placement="inline" />
|
||||||
totalPageCount={pagination.totalPageCount}
|
|
||||||
currentPage={pagination.currentPage}
|
|
||||||
nextPage={pagination.nextPage}
|
|
||||||
prevPage={pagination.prevPage}
|
|
||||||
/>
|
|
||||||
</StageWrapper>
|
</StageWrapper>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -31,35 +31,20 @@ export const useSidePanel = () => {
|
|||||||
const isSidePanelOpen = !!activePanelId
|
const isSidePanelOpen = !!activePanelId
|
||||||
const isSubPanelOpen = !!activeSubPanelId
|
const isSubPanelOpen = !!activeSubPanelId
|
||||||
|
|
||||||
const toggleAdmin = () => {
|
// Reads the live store, not the render snapshot: shortcut handlers outlive
|
||||||
layoutStore.activePanelId = isAdminOpen ? null : PanelId.ADMIN
|
// toggles that unmount (e.g. inside a closed overflow menu).
|
||||||
if (layoutSnap.activeSubPanelId) layoutStore.activeSubPanelId = null
|
const togglePanel = (panelId: PanelId) => {
|
||||||
|
layoutStore.activePanelId =
|
||||||
|
layoutStore.activePanelId === panelId ? null : panelId
|
||||||
|
if (layoutStore.activeSubPanelId) layoutStore.activeSubPanelId = null
|
||||||
}
|
}
|
||||||
|
|
||||||
const toggleParticipants = () => {
|
const toggleAdmin = () => togglePanel(PanelId.ADMIN)
|
||||||
layoutStore.activePanelId = isParticipantsOpen ? null : PanelId.PARTICIPANTS
|
const toggleParticipants = () => togglePanel(PanelId.PARTICIPANTS)
|
||||||
if (layoutSnap.activeSubPanelId) layoutStore.activeSubPanelId = null
|
const toggleChat = () => togglePanel(PanelId.CHAT)
|
||||||
}
|
const toggleEffects = () => togglePanel(PanelId.EFFECTS)
|
||||||
|
const toggleTools = () => togglePanel(PanelId.TOOLS)
|
||||||
const toggleChat = () => {
|
const toggleInfo = () => togglePanel(PanelId.INFO)
|
||||||
layoutStore.activePanelId = isChatOpen ? null : PanelId.CHAT
|
|
||||||
if (layoutSnap.activeSubPanelId) layoutStore.activeSubPanelId = null
|
|
||||||
}
|
|
||||||
|
|
||||||
const toggleEffects = () => {
|
|
||||||
layoutStore.activePanelId = isEffectsOpen ? null : PanelId.EFFECTS
|
|
||||||
if (layoutSnap.activeSubPanelId) layoutStore.activeSubPanelId = null
|
|
||||||
}
|
|
||||||
|
|
||||||
const toggleTools = () => {
|
|
||||||
layoutStore.activePanelId = isToolsOpen ? null : PanelId.TOOLS
|
|
||||||
if (layoutSnap.activeSubPanelId) layoutStore.activeSubPanelId = null
|
|
||||||
}
|
|
||||||
|
|
||||||
const toggleInfo = () => {
|
|
||||||
layoutStore.activePanelId = isInfoOpen ? null : PanelId.INFO
|
|
||||||
if (layoutSnap.activeSubPanelId) layoutStore.activeSubPanelId = null
|
|
||||||
}
|
|
||||||
|
|
||||||
const openTranscript = () => {
|
const openTranscript = () => {
|
||||||
layoutStore.activeSubPanelId = SubPanelId.TRANSCRIPT
|
layoutStore.activeSubPanelId = SubPanelId.TRANSCRIPT
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
import { normalizeRoomId } from './isRoomValid'
|
||||||
|
|
||||||
|
describe('normalizeRoomId', () => {
|
||||||
|
it('lowercases and re-inserts the hyphens of a ten-letter id', () => {
|
||||||
|
expect(normalizeRoomId('ABCDEFGHIJ')).toBe('abc-defg-hij')
|
||||||
|
expect(normalizeRoomId('abc-defghij')).toBe('abc-defg-hij')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns any other input unchanged', () => {
|
||||||
|
expect(normalizeRoomId('abc-def')).toBe('abc-def')
|
||||||
|
expect(normalizeRoomId('Not-A-Room')).toBe('Not-A-Room')
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -7,6 +7,7 @@ export type ShortcutCategory = 'navigation' | 'media' | 'interaction'
|
|||||||
export type ShortcutId =
|
export type ShortcutId =
|
||||||
| 'open-shortcuts'
|
| 'open-shortcuts'
|
||||||
| 'focus-toolbar'
|
| 'focus-toolbar'
|
||||||
|
| 'focus-pagination'
|
||||||
| 'toggle-microphone'
|
| 'toggle-microphone'
|
||||||
| 'toggle-camera'
|
| 'toggle-camera'
|
||||||
| 'push-to-talk'
|
| 'push-to-talk'
|
||||||
@@ -44,6 +45,11 @@ export const shortcutCatalog: ShortcutDescriptor[] = [
|
|||||||
category: 'navigation',
|
category: 'navigation',
|
||||||
shortcut: { key: 'F2' },
|
shortcut: { key: 'F2' },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'focus-pagination',
|
||||||
|
category: 'navigation',
|
||||||
|
shortcut: { key: 'G', ctrlKey: true, shiftKey: true },
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'toggle-microphone',
|
id: 'toggle-microphone',
|
||||||
category: 'media',
|
category: 'media',
|
||||||
|
|||||||
@@ -7,12 +7,16 @@ export type useRegisterKeyboardShortcutProps = {
|
|||||||
id?: ShortcutId
|
id?: ShortcutId
|
||||||
handler: () => Promise<void | boolean | undefined> | void
|
handler: () => Promise<void | boolean | undefined> | void
|
||||||
isDisabled?: boolean
|
isDisabled?: boolean
|
||||||
|
// Opt-in: controls rendered inside menus unmount when the menu closes, and
|
||||||
|
// their shortcuts must keep working.
|
||||||
|
unregisterOnUnmount?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useRegisterKeyboardShortcut = ({
|
export const useRegisterKeyboardShortcut = ({
|
||||||
id,
|
id,
|
||||||
handler,
|
handler,
|
||||||
isDisabled = false,
|
isDisabled = false,
|
||||||
|
unregisterOnUnmount = false,
|
||||||
}: useRegisterKeyboardShortcutProps) => {
|
}: useRegisterKeyboardShortcutProps) => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!id) return
|
if (!id) return
|
||||||
@@ -21,8 +25,14 @@ export const useRegisterKeyboardShortcut = ({
|
|||||||
const formattedKey = formatShortcutKey(descriptor.shortcut)
|
const formattedKey = formatShortcutKey(descriptor.shortcut)
|
||||||
if (isDisabled) {
|
if (isDisabled) {
|
||||||
keyboardShortcutsStore.shortcuts.delete(formattedKey)
|
keyboardShortcutsStore.shortcuts.delete(formattedKey)
|
||||||
} else {
|
return
|
||||||
keyboardShortcutsStore.shortcuts.set(formattedKey, handler)
|
|
||||||
}
|
}
|
||||||
}, [handler, id, isDisabled])
|
keyboardShortcutsStore.shortcuts.set(formattedKey, handler)
|
||||||
|
if (!unregisterOnUnmount) return
|
||||||
|
return () => {
|
||||||
|
if (keyboardShortcutsStore.shortcuts.get(formattedKey) === handler) {
|
||||||
|
keyboardShortcutsStore.shortcuts.delete(formattedKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [handler, id, isDisabled, unregisterOnUnmount])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -784,6 +784,7 @@
|
|||||||
"actions": {
|
"actions": {
|
||||||
"open-shortcuts": "Tastenkürzel-Hilfe öffnen",
|
"open-shortcuts": "Tastenkürzel-Hilfe öffnen",
|
||||||
"focus-toolbar": "Fokus auf die untere Symbolleiste",
|
"focus-toolbar": "Fokus auf die untere Symbolleiste",
|
||||||
|
"focus-pagination": "Fokus auf die Teilnehmerseiten",
|
||||||
"toggle-microphone": "Mikrofon umschalten",
|
"toggle-microphone": "Mikrofon umschalten",
|
||||||
"toggle-camera": "Kamera umschalten",
|
"toggle-camera": "Kamera umschalten",
|
||||||
"push-to-talk": "Push-to-talk",
|
"push-to-talk": "Push-to-talk",
|
||||||
|
|||||||
@@ -784,6 +784,7 @@
|
|||||||
"actions": {
|
"actions": {
|
||||||
"open-shortcuts": "Open shortcuts help",
|
"open-shortcuts": "Open shortcuts help",
|
||||||
"focus-toolbar": "Focus bottom toolbar",
|
"focus-toolbar": "Focus bottom toolbar",
|
||||||
|
"focus-pagination": "Focus participant pagination",
|
||||||
"toggle-microphone": "Toggle microphone",
|
"toggle-microphone": "Toggle microphone",
|
||||||
"toggle-camera": "Toggle camera",
|
"toggle-camera": "Toggle camera",
|
||||||
"push-to-talk": "Push-to-talk (hold to unmute)",
|
"push-to-talk": "Push-to-talk (hold to unmute)",
|
||||||
|
|||||||
@@ -783,6 +783,7 @@
|
|||||||
"actions": {
|
"actions": {
|
||||||
"open-shortcuts": "Abrir la ayuda de atajos",
|
"open-shortcuts": "Abrir la ayuda de atajos",
|
||||||
"focus-toolbar": "Poner el foco en la barra de herramientas inferior",
|
"focus-toolbar": "Poner el foco en la barra de herramientas inferior",
|
||||||
|
"focus-pagination": "Poner el foco en la paginación de participantes",
|
||||||
"toggle-microphone": "Activar o desactivar el micrófono",
|
"toggle-microphone": "Activar o desactivar el micrófono",
|
||||||
"toggle-camera": "Activar o desactivar la cámara",
|
"toggle-camera": "Activar o desactivar la cámara",
|
||||||
"push-to-talk": "Pulsar para hablar (mantener para reactivar)",
|
"push-to-talk": "Pulsar para hablar (mantener para reactivar)",
|
||||||
|
|||||||
@@ -784,6 +784,7 @@
|
|||||||
"actions": {
|
"actions": {
|
||||||
"open-shortcuts": "Ouvrir l’aide des raccourcis",
|
"open-shortcuts": "Ouvrir l’aide des raccourcis",
|
||||||
"focus-toolbar": "Mettre le focus sur la barre d’outils du bas",
|
"focus-toolbar": "Mettre le focus sur la barre d’outils du bas",
|
||||||
|
"focus-pagination": "Mettre le focus sur la pagination des participants",
|
||||||
"toggle-microphone": "Activer ou désactiver le micro",
|
"toggle-microphone": "Activer ou désactiver le micro",
|
||||||
"toggle-camera": "Activer ou désactiver la caméra",
|
"toggle-camera": "Activer ou désactiver la caméra",
|
||||||
"push-to-talk": "Appuyer pour parler (maintenir pour réactiver)",
|
"push-to-talk": "Appuyer pour parler (maintenir pour réactiver)",
|
||||||
|
|||||||
@@ -784,6 +784,7 @@
|
|||||||
"actions": {
|
"actions": {
|
||||||
"open-shortcuts": "Sneltoetsenhulp openen",
|
"open-shortcuts": "Sneltoetsenhulp openen",
|
||||||
"focus-toolbar": "Focus op de onderste werkbalk",
|
"focus-toolbar": "Focus op de onderste werkbalk",
|
||||||
|
"focus-pagination": "Focus op de paginering van deelnemers",
|
||||||
"toggle-microphone": "Microfoon aan/uit",
|
"toggle-microphone": "Microfoon aan/uit",
|
||||||
"toggle-camera": "Camera aan/uit",
|
"toggle-camera": "Camera aan/uit",
|
||||||
"push-to-talk": "Push-to-talk (ingedrukt houden om te activeren)",
|
"push-to-talk": "Push-to-talk (ingedrukt houden om te activeren)",
|
||||||
|
|||||||
@@ -10,5 +10,5 @@
|
|||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"types": ["node"]
|
"types": ["node"]
|
||||||
},
|
},
|
||||||
"include": ["vite.config.ts"]
|
"include": ["vite.config.ts", "vitest.config.ts"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { defineConfig } from 'vitest/config'
|
||||||
|
|
||||||
|
// Kept apart from vite.config.ts so the tests load none of the build plugins.
|
||||||
|
export default defineConfig({
|
||||||
|
resolve: {
|
||||||
|
tsconfigPaths: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -24,7 +24,7 @@ _summaryEnvVars: &summaryEnvVars
|
|||||||
APP_NAME: summary-microservice
|
APP_NAME: summary-microservice
|
||||||
APP_API_TOKEN: password
|
APP_API_TOKEN: password
|
||||||
AWS_STORAGE_BUCKET_NAME: meet-media-storage
|
AWS_STORAGE_BUCKET_NAME: meet-media-storage
|
||||||
AWS_S3_ENDPOINT_URL: http://garage.meet.svc.cluster.local:9000/
|
AWS_S3_ENDPOINT_URL: http://dev-backend-garage.meet.svc.cluster.local:9000/
|
||||||
AWS_S3_ACCESS_KEY_ID: meet-access-key
|
AWS_S3_ACCESS_KEY_ID: meet-access-key
|
||||||
AWS_S3_SECRET_ACCESS_KEY: meet-secret-access-key
|
AWS_S3_SECRET_ACCESS_KEY: meet-secret-access-key
|
||||||
AWS_S3_SECURE_ACCESS: False
|
AWS_S3_SECURE_ACCESS: False
|
||||||
@@ -69,9 +69,9 @@ _summaryEnvVars: &summaryEnvVars
|
|||||||
LLM_MODEL: Qwen/Qwen3-Coder-30B-A3B-Instruct
|
LLM_MODEL: Qwen/Qwen3-Coder-30B-A3B-Instruct
|
||||||
WEBHOOK_API_TOKEN: password
|
WEBHOOK_API_TOKEN: password
|
||||||
WEBHOOK_URL: https://www.mock-impress.com/webhook/
|
WEBHOOK_URL: https://www.mock-impress.com/webhook/
|
||||||
CELERY_BROKER_URL: redis://default:pass@redis-master:6379/1
|
CELERY_BROKER_URL: redis://user:pass@dev-backend-redis:6379/1
|
||||||
CELERY_RESULT_BACKEND: redis://default:pass@redis-master:6379/1
|
CELERY_RESULT_BACKEND: redis://user:pass@dev-backend-redis:6379/1
|
||||||
TASK_TRACKER_REDIS_URL: redis://default:pass@redis-master:6379/1
|
TASK_TRACKER_REDIS_URL: redis://user:pass@dev-backend-redis:6379/1
|
||||||
IS_RESOLVE_SPEAKER_IDENTITIES_ENABLED: true
|
IS_RESOLVE_SPEAKER_IDENTITIES_ENABLED: true
|
||||||
RESOLVE_SPEAKER_IDENTITIES_DEFAULT_OVERLAP: 0.5
|
RESOLVE_SPEAKER_IDENTITIES_DEFAULT_OVERLAP: 0.5
|
||||||
RESOLVE_SPEAKER_ENABLE_SPLIT_ON_WORDS: true
|
RESOLVE_SPEAKER_ENABLE_SPLIT_ON_WORDS: true
|
||||||
@@ -127,12 +127,21 @@ backend:
|
|||||||
LOGIN_REDIRECT_URL_FAILURE: https://meet.127.0.0.1.nip.io
|
LOGIN_REDIRECT_URL_FAILURE: https://meet.127.0.0.1.nip.io
|
||||||
LOGOUT_REDIRECT_URL: https://meet.127.0.0.1.nip.io
|
LOGOUT_REDIRECT_URL: https://meet.127.0.0.1.nip.io
|
||||||
# Databases
|
# Databases
|
||||||
DB_HOST: postgres
|
DB_HOST: dev-backend-postgres
|
||||||
DB_NAME: meet
|
DB_NAME:
|
||||||
DB_USER: dinum
|
secretKeyRef:
|
||||||
DB_PASSWORD: pass
|
name: dev-backend-postgres
|
||||||
|
key: database
|
||||||
|
DB_USER:
|
||||||
|
secretKeyRef:
|
||||||
|
name: dev-backend-postgres
|
||||||
|
key: username
|
||||||
|
DB_PASSWORD:
|
||||||
|
secretKeyRef:
|
||||||
|
name: dev-backend-postgres
|
||||||
|
key: password
|
||||||
DB_PORT: 5432
|
DB_PORT: 5432
|
||||||
REDIS_URL: redis://default:pass@redis-master:6379/1
|
REDIS_URL: redis://user:pass@dev-backend-redis:6379/1
|
||||||
# Static files
|
# Static files
|
||||||
STORAGES_STATICFILES_BACKEND: django.contrib.staticfiles.storage.StaticFilesStorage
|
STORAGES_STATICFILES_BACKEND: django.contrib.staticfiles.storage.StaticFilesStorage
|
||||||
# Permissions
|
# Permissions
|
||||||
@@ -163,7 +172,7 @@ backend:
|
|||||||
FRONTEND_TRANSCRIPTION_DESTINATION: "https://docs.numerique.gouv.fr"
|
FRONTEND_TRANSCRIPTION_DESTINATION: "https://docs.numerique.gouv.fr"
|
||||||
FRONTEND_IS_SILENT_LOGIN_ENABLED: False
|
FRONTEND_IS_SILENT_LOGIN_ENABLED: False
|
||||||
# S3 Storage
|
# S3 Storage
|
||||||
AWS_S3_ENDPOINT_URL: http://garage.meet.svc.cluster.local:9000
|
AWS_S3_ENDPOINT_URL: http://dev-backend-garage.meet.svc.cluster.local:9000
|
||||||
AWS_S3_ACCESS_KEY_ID: meet-access-key
|
AWS_S3_ACCESS_KEY_ID: meet-access-key
|
||||||
AWS_S3_SECRET_ACCESS_KEY: meet-secret-access-key
|
AWS_S3_SECRET_ACCESS_KEY: meet-secret-access-key
|
||||||
AWS_STORAGE_BUCKET_NAME: meet-media-storage
|
AWS_STORAGE_BUCKET_NAME: meet-media-storage
|
||||||
@@ -186,7 +195,7 @@ backend:
|
|||||||
MEDIA_BASE_URL: https://meet.127.0.0.1.nip.io
|
MEDIA_BASE_URL: https://meet.127.0.0.1.nip.io
|
||||||
FILE_UPLOAD_ENABLED: True
|
FILE_UPLOAD_ENABLED: True
|
||||||
CELERY_ENABLED: True
|
CELERY_ENABLED: True
|
||||||
CELERY_BROKER_URL: redis://default:pass@redis-master:6379/1
|
CELERY_BROKER_URL: redis://user:pass@dev-backend-redis:6379/1
|
||||||
# Recording & Transcription
|
# Recording & Transcription
|
||||||
RECORDING_ENABLE: True
|
RECORDING_ENABLE: True
|
||||||
SUMMARY_SERVICE_ENDPOINT: http://meet-summary:80/api/v2/async-jobs/transcribe/
|
SUMMARY_SERVICE_ENDPOINT: http://meet-summary:80/api/v2/async-jobs/transcribe/
|
||||||
@@ -266,11 +275,11 @@ ingressMedia:
|
|||||||
annotations:
|
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-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/auth-response-headers: "Authorization, X-Amz-Date, X-Amz-Content-SHA256"
|
||||||
nginx.ingress.kubernetes.io/upstream-vhost: garage.meet.svc.cluster.local:9000
|
nginx.ingress.kubernetes.io/upstream-vhost: dev-backend-garage.meet.svc.cluster.local:9000
|
||||||
nginx.ingress.kubernetes.io/rewrite-target: /meet-media-storage/$1
|
nginx.ingress.kubernetes.io/rewrite-target: /meet-media-storage/$1
|
||||||
|
|
||||||
serviceMedia:
|
serviceMedia:
|
||||||
host: garage.meet.svc.cluster.local
|
host: dev-backend-garage.meet.svc.cluster.local
|
||||||
port: 9000
|
port: 9000
|
||||||
|
|
||||||
# ---- Extra ingress/service for background file uploads ------------
|
# ---- Extra ingress/service for background file uploads ------------
|
||||||
@@ -282,11 +291,11 @@ ingressMediaFiles:
|
|||||||
annotations:
|
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-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/auth-response-headers: "Authorization, X-Amz-Date, X-Amz-Content-SHA256"
|
||||||
nginx.ingress.kubernetes.io/upstream-vhost: garage.meet.svc.cluster.local:9000
|
nginx.ingress.kubernetes.io/upstream-vhost: dev-backend-garage.meet.svc.cluster.local:9000
|
||||||
nginx.ingress.kubernetes.io/rewrite-target: /meet-media-storage/files/$1
|
nginx.ingress.kubernetes.io/rewrite-target: /meet-media-storage/files/$1
|
||||||
|
|
||||||
serviceMediaFiles:
|
serviceMediaFiles:
|
||||||
host: garage.meet.svc.cluster.local
|
host: dev-backend-garage.meet.svc.cluster.local
|
||||||
port: 9000
|
port: 9000
|
||||||
|
|
||||||
# ---- STT Orchestration Microservice Components --------------------
|
# ---- STT Orchestration Microservice Components --------------------
|
||||||
@@ -364,7 +373,7 @@ agentMetadata:
|
|||||||
{{- end }}
|
{{- end }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
ENABLE_SILERO_VAD: "false"
|
ENABLE_SILERO_VAD: "false"
|
||||||
AWS_S3_ENDPOINT_URL: garage.meet.svc.cluster.local:9000
|
AWS_S3_ENDPOINT_URL: dev-backend-garage.meet.svc.cluster.local:9000
|
||||||
AWS_S3_ACCESS_KEY_ID: meet-access-key
|
AWS_S3_ACCESS_KEY_ID: meet-access-key
|
||||||
AWS_S3_SECRET_ACCESS_KEY: meet-secret-access-key
|
AWS_S3_SECRET_ACCESS_KEY: meet-secret-access-key
|
||||||
AWS_S3_SECURE_ACCESS: False
|
AWS_S3_SECURE_ACCESS: False
|
||||||
|
|||||||
@@ -13,14 +13,15 @@ egress:
|
|||||||
{{- end }}
|
{{- end }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
redis:
|
redis:
|
||||||
address: redis-master:6379
|
address: dev-backend-redis:6379
|
||||||
|
username: user
|
||||||
password: pass
|
password: pass
|
||||||
s3:
|
s3:
|
||||||
access_key: meet-access-key
|
access_key: meet-access-key
|
||||||
secret: meet-secret-access-key
|
secret: meet-secret-access-key
|
||||||
region: local
|
region: local
|
||||||
bucket: meet-media-storage
|
bucket: meet-media-storage
|
||||||
endpoint: http://garage:9000
|
endpoint: http://dev-backend-garage:9000
|
||||||
force_path_style: true
|
force_path_style: true
|
||||||
|
|
||||||
loadBalancer:
|
loadBalancer:
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ livekit:
|
|||||||
port_range_end: 60000
|
port_range_end: 60000
|
||||||
tcp_port: 7881
|
tcp_port: 7881
|
||||||
redis:
|
redis:
|
||||||
address: redis-master:6379
|
address: dev-backend-redis:6379
|
||||||
|
username: user
|
||||||
password: pass
|
password: pass
|
||||||
keys:
|
keys:
|
||||||
turn:
|
turn:
|
||||||
|
|||||||
@@ -16,14 +16,15 @@ egress:
|
|||||||
{{- end }}
|
{{- end }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
redis:
|
redis:
|
||||||
address: redis-master:6379
|
address: dev-backend-redis:6379
|
||||||
|
username: user
|
||||||
password: pass
|
password: pass
|
||||||
s3:
|
s3:
|
||||||
access_key: meet-access-key
|
access_key: meet-access-key
|
||||||
secret: meet-secret-access-key
|
secret: meet-secret-access-key
|
||||||
region: local
|
region: local
|
||||||
bucket: meet-media-storage
|
bucket: meet-media-storage
|
||||||
endpoint: http://garage:9000
|
endpoint: http://dev-backend-garage:9000
|
||||||
force_path_style: true
|
force_path_style: true
|
||||||
|
|
||||||
loadBalancer:
|
loadBalancer:
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ livekit:
|
|||||||
port_range_end: 60000
|
port_range_end: 60000
|
||||||
tcp_port: 7881
|
tcp_port: 7881
|
||||||
redis:
|
redis:
|
||||||
address: redis-master:6379
|
address: dev-backend-redis:6379
|
||||||
|
username: user
|
||||||
password: pass
|
password: pass
|
||||||
keys:
|
keys:
|
||||||
turn:
|
turn:
|
||||||
|
|||||||
@@ -1,172 +0,0 @@
|
|||||||
---
|
|
||||||
apiVersion: networking.k8s.io/v1
|
|
||||||
kind: Ingress
|
|
||||||
metadata:
|
|
||||||
name: garage
|
|
||||||
annotations:
|
|
||||||
nginx.ingress.kubernetes.io/proxy-body-size: 10m
|
|
||||||
spec:
|
|
||||||
rules:
|
|
||||||
- host: "garage.127.0.0.1.nip.io"
|
|
||||||
http:
|
|
||||||
paths:
|
|
||||||
- path: /
|
|
||||||
pathType: Prefix
|
|
||||||
backend:
|
|
||||||
service:
|
|
||||||
name: garage
|
|
||||||
port:
|
|
||||||
number: 9000
|
|
||||||
tls:
|
|
||||||
- hosts:
|
|
||||||
- garage.127.0.0.1.nip.io
|
|
||||||
secretName: meet-tls
|
|
||||||
---
|
|
||||||
apiVersion: v1
|
|
||||||
kind: Service
|
|
||||||
metadata:
|
|
||||||
name: garage
|
|
||||||
namespace: {{ .Release.Namespace | quote }}
|
|
||||||
spec:
|
|
||||||
ports:
|
|
||||||
- name: client
|
|
||||||
port: 9000
|
|
||||||
protocol: TCP
|
|
||||||
targetPort: 9000
|
|
||||||
selector:
|
|
||||||
app.kubernetes.io/instance: extra
|
|
||||||
app.kubernetes.io/name: garage
|
|
||||||
type: ClusterIP
|
|
||||||
---
|
|
||||||
apiVersion: v1
|
|
||||||
kind: ConfigMap
|
|
||||||
metadata:
|
|
||||||
name: garage-config
|
|
||||||
namespace: {{ .Release.Namespace | quote }}
|
|
||||||
data:
|
|
||||||
garage.toml: |
|
|
||||||
metadata_dir = "/var/lib/garage/meta"
|
|
||||||
data_dir = "/var/lib/garage/data"
|
|
||||||
db_engine = "lmdb"
|
|
||||||
|
|
||||||
replication_factor = 1
|
|
||||||
|
|
||||||
rpc_bind_addr = "127.0.0.1:3901"
|
|
||||||
rpc_public_addr = "127.0.0.1:3901"
|
|
||||||
|
|
||||||
[s3_api]
|
|
||||||
api_bind_addr = "[::]:9000"
|
|
||||||
# Clients must sign their requests for this region (AWS_S3_REGION_NAME)
|
|
||||||
s3_region = "local"
|
|
||||||
---
|
|
||||||
apiVersion: v1
|
|
||||||
kind: Secret
|
|
||||||
metadata:
|
|
||||||
name: garage-dev
|
|
||||||
namespace: {{ .Release.Namespace | quote }}
|
|
||||||
type: Opaque
|
|
||||||
data:
|
|
||||||
GARAGE_RPC_SECRET: {{ printf "%s/garage-rpc-secret" .Release.Namespace | sha256sum | b64enc }}
|
|
||||||
---
|
|
||||||
apiVersion: apps/v1
|
|
||||||
kind: Deployment
|
|
||||||
metadata:
|
|
||||||
name: garage
|
|
||||||
namespace: {{ .Release.Namespace | quote }}
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/instance: extra
|
|
||||||
app.kubernetes.io/name: garage
|
|
||||||
spec:
|
|
||||||
selector:
|
|
||||||
matchLabels:
|
|
||||||
app.kubernetes.io/instance: extra
|
|
||||||
app.kubernetes.io/name: garage
|
|
||||||
replicas: 1
|
|
||||||
template:
|
|
||||||
metadata:
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/instance: extra
|
|
||||||
app.kubernetes.io/name: garage
|
|
||||||
spec:
|
|
||||||
containers:
|
|
||||||
- name: garage
|
|
||||||
command:
|
|
||||||
- /garage
|
|
||||||
- server
|
|
||||||
- --single-node
|
|
||||||
- --default-bucket
|
|
||||||
env:
|
|
||||||
- name: GARAGE_RPC_SECRET
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: garage-dev
|
|
||||||
key: GARAGE_RPC_SECRET
|
|
||||||
- name: GARAGE_DEFAULT_ACCESS_KEY
|
|
||||||
value: meet-access-key
|
|
||||||
- name: GARAGE_DEFAULT_SECRET_KEY
|
|
||||||
value: meet-secret-access-key
|
|
||||||
- name: GARAGE_DEFAULT_BUCKET
|
|
||||||
value: meet-media-storage
|
|
||||||
image: "dxflrs/garage:v2.4.1"
|
|
||||||
imagePullPolicy: IfNotPresent
|
|
||||||
ports:
|
|
||||||
- containerPort: 9000
|
|
||||||
name: client
|
|
||||||
readinessProbe:
|
|
||||||
exec:
|
|
||||||
command:
|
|
||||||
- /garage
|
|
||||||
- health
|
|
||||||
volumeMounts:
|
|
||||||
- mountPath: /etc/garage.toml
|
|
||||||
name: config
|
|
||||||
subPath: garage.toml
|
|
||||||
- mountPath: /var/lib/garage
|
|
||||||
name: data
|
|
||||||
volumes:
|
|
||||||
- name: config
|
|
||||||
configMap:
|
|
||||||
name: garage-config
|
|
||||||
- name: data
|
|
||||||
emptyDir:
|
|
||||||
---
|
|
||||||
# Garage denies cross-origin requests by default: allow the frontend to upload
|
|
||||||
# files straight to the bucket
|
|
||||||
apiVersion: batch/v1
|
|
||||||
kind: Job
|
|
||||||
metadata:
|
|
||||||
name: garage-cors
|
|
||||||
spec:
|
|
||||||
template:
|
|
||||||
spec:
|
|
||||||
containers:
|
|
||||||
- name: aws-cli
|
|
||||||
image: amazon/aws-cli:2.37.1
|
|
||||||
env:
|
|
||||||
- name: AWS_ACCESS_KEY_ID
|
|
||||||
value: meet-access-key
|
|
||||||
- name: AWS_SECRET_ACCESS_KEY
|
|
||||||
value: meet-secret-access-key
|
|
||||||
- name: AWS_DEFAULT_REGION
|
|
||||||
value: local
|
|
||||||
- name: AWS_ENDPOINT_URL
|
|
||||||
value: http://garage:9000
|
|
||||||
- name: BUCKET
|
|
||||||
value: meet-media-storage
|
|
||||||
command:
|
|
||||||
- /bin/sh
|
|
||||||
- -c
|
|
||||||
- |
|
|
||||||
deadline=$(($(date +%s) + 300))
|
|
||||||
until aws s3api head-bucket --bucket="$BUCKET" --cli-connect-timeout=5; do
|
|
||||||
if [ "$(date +%s)" -ge "$deadline" ]; then
|
|
||||||
echo "Bucket $BUCKET still unavailable on $AWS_ENDPOINT_URL" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo "Waiting for bucket $BUCKET on $AWS_ENDPOINT_URL"
|
|
||||||
sleep 5
|
|
||||||
done
|
|
||||||
exec aws s3api put-bucket-cors --bucket="$BUCKET" \
|
|
||||||
--cors-configuration='{"CORSRules": [{"AllowedOrigins": ["https://meet.127.0.0.1.nip.io"], "AllowedMethods": ["GET", "HEAD", "PUT"], "AllowedHeaders": ["*"], "ExposeHeaders": ["ETag"]}]}'
|
|
||||||
restartPolicy: Never
|
|
||||||
backoffLimit: 3
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
---
|
|
||||||
apiVersion: v1
|
|
||||||
kind: Service
|
|
||||||
metadata:
|
|
||||||
name: kc-postgres
|
|
||||||
namespace: {{ .Release.Namespace | quote }}
|
|
||||||
spec:
|
|
||||||
ports:
|
|
||||||
- name: tcp-postgresql
|
|
||||||
port: 5432
|
|
||||||
protocol: TCP
|
|
||||||
targetPort: tcp-postgresql
|
|
||||||
selector:
|
|
||||||
app.kubernetes.io/instance: extra
|
|
||||||
app.kubernetes.io/name: kc-postgresql
|
|
||||||
type: ClusterIP
|
|
||||||
---
|
|
||||||
apiVersion: apps/v1
|
|
||||||
kind: StatefulSet
|
|
||||||
metadata:
|
|
||||||
name: kc-postgresql
|
|
||||||
namespace: {{ .Release.Namespace | quote }}
|
|
||||||
spec:
|
|
||||||
selector:
|
|
||||||
matchLabels:
|
|
||||||
app.kubernetes.io/instance: extra
|
|
||||||
app.kubernetes.io/name: kc-postgresql
|
|
||||||
serviceName: "kc-postgres"
|
|
||||||
replicas: 1
|
|
||||||
template:
|
|
||||||
metadata:
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/instance: extra
|
|
||||||
app.kubernetes.io/name: kc-postgresql
|
|
||||||
spec:
|
|
||||||
terminationGracePeriodSeconds: 10
|
|
||||||
containers:
|
|
||||||
- name: pg
|
|
||||||
image: postgres:16-alpine
|
|
||||||
ports:
|
|
||||||
- containerPort: 5432
|
|
||||||
name: tcp-postgresql
|
|
||||||
env:
|
|
||||||
- name: POSTGRES_PASSWORD
|
|
||||||
value: pass
|
|
||||||
- name: POSTGRES_USER
|
|
||||||
value: dinum
|
|
||||||
- name: POSTGRES_DB
|
|
||||||
value: keycloak
|
|
||||||
volumeMounts:
|
|
||||||
- name: data
|
|
||||||
mountPath: /var/lib/postgresql
|
|
||||||
volumeClaimTemplates:
|
|
||||||
- metadata:
|
|
||||||
name: data
|
|
||||||
spec:
|
|
||||||
accessModes: [ "ReadWriteOnce" ]
|
|
||||||
resources:
|
|
||||||
requests:
|
|
||||||
storage: 1Gi
|
|
||||||
|
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
---
|
|
||||||
apiVersion: networking.k8s.io/v1
|
|
||||||
kind: Ingress
|
|
||||||
metadata:
|
|
||||||
name: keycloak
|
|
||||||
spec:
|
|
||||||
rules:
|
|
||||||
- host: "keycloak.127.0.0.1.nip.io"
|
|
||||||
http:
|
|
||||||
paths:
|
|
||||||
- path: /
|
|
||||||
pathType: Prefix
|
|
||||||
backend:
|
|
||||||
service:
|
|
||||||
name: keycloak
|
|
||||||
port:
|
|
||||||
number: 8080
|
|
||||||
tls:
|
|
||||||
- hosts:
|
|
||||||
- keycloak.127.0.0.1.nip.io
|
|
||||||
secretName: meet-tls
|
|
||||||
---
|
|
||||||
apiVersion: v1
|
|
||||||
kind: Service
|
|
||||||
metadata:
|
|
||||||
name: keycloak
|
|
||||||
namespace: {{ .Release.Namespace | quote }}
|
|
||||||
spec:
|
|
||||||
ports:
|
|
||||||
- name: tcp-keycloak
|
|
||||||
port: 8080
|
|
||||||
protocol: TCP
|
|
||||||
targetPort: tcp-keycloak
|
|
||||||
selector:
|
|
||||||
app.kubernetes.io/instance: extra
|
|
||||||
app.kubernetes.io/name: keycloak
|
|
||||||
type: ClusterIP
|
|
||||||
---
|
|
||||||
apiVersion: v1
|
|
||||||
kind: ConfigMap
|
|
||||||
metadata:
|
|
||||||
name: realm
|
|
||||||
data:
|
|
||||||
meet.json: |
|
|
||||||
{{ .Values.realm | indent 4 }}
|
|
||||||
---
|
|
||||||
apiVersion: apps/v1
|
|
||||||
kind: StatefulSet
|
|
||||||
metadata:
|
|
||||||
name: keycloak
|
|
||||||
namespace: {{ .Release.Namespace | quote }}
|
|
||||||
spec:
|
|
||||||
selector:
|
|
||||||
matchLabels:
|
|
||||||
app.kubernetes.io/instance: extra
|
|
||||||
app.kubernetes.io/name: keycloak
|
|
||||||
serviceName: "keycloak"
|
|
||||||
replicas: 1
|
|
||||||
template:
|
|
||||||
metadata:
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/instance: extra
|
|
||||||
app.kubernetes.io/name: keycloak
|
|
||||||
spec:
|
|
||||||
terminationGracePeriodSeconds: 10
|
|
||||||
containers:
|
|
||||||
- name: keycloak
|
|
||||||
image: quay.io/keycloak/keycloak:20.0.1
|
|
||||||
args:
|
|
||||||
- start-dev
|
|
||||||
- --features=preview
|
|
||||||
- --import-realm
|
|
||||||
- --proxy=edge
|
|
||||||
- --hostname=keycloak.127.0.0.1.nip.io
|
|
||||||
- --hostname-strict=false
|
|
||||||
- --hostname-strict-https=false
|
|
||||||
ports:
|
|
||||||
- containerPort: 8080
|
|
||||||
name: tcp-keycloak
|
|
||||||
env:
|
|
||||||
- name: KEYCLOAK_ADMIN
|
|
||||||
value: admin
|
|
||||||
- name: KEYCLOAK_ADMIN_PASSWORD
|
|
||||||
value: admin
|
|
||||||
- name: PROXY_ADDRESS_FORWARDING
|
|
||||||
value: 'true'
|
|
||||||
- name: KC_DB_URL_HOST
|
|
||||||
value: kc_postgresql
|
|
||||||
- name: KC_DB_URL_DATABASE
|
|
||||||
value: keycloak
|
|
||||||
- name: KC_DB_PASSWORD
|
|
||||||
value: pass
|
|
||||||
- name: KC_DB_USERNAME
|
|
||||||
value: dinum
|
|
||||||
- name: KC_DB_SCHEMA
|
|
||||||
value: public
|
|
||||||
volumeMounts:
|
|
||||||
- name: realm
|
|
||||||
mountPath: "/opt/keycloak/data/import"
|
|
||||||
readOnly: true
|
|
||||||
volumes:
|
|
||||||
- name: realm
|
|
||||||
configMap:
|
|
||||||
name: realm
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
---
|
|
||||||
apiVersion: v1
|
|
||||||
kind: Service
|
|
||||||
metadata:
|
|
||||||
name: postgres
|
|
||||||
namespace: {{ .Release.Namespace | quote }}
|
|
||||||
spec:
|
|
||||||
ports:
|
|
||||||
- name: tcp-postgresql
|
|
||||||
port: 5432
|
|
||||||
protocol: TCP
|
|
||||||
targetPort: tcp-postgresql
|
|
||||||
selector:
|
|
||||||
app.kubernetes.io/instance: extra
|
|
||||||
app.kubernetes.io/name: postgresql
|
|
||||||
type: ClusterIP
|
|
||||||
---
|
|
||||||
apiVersion: apps/v1
|
|
||||||
kind: StatefulSet
|
|
||||||
metadata:
|
|
||||||
name: postgresql
|
|
||||||
namespace: {{ .Release.Namespace | quote }}
|
|
||||||
spec:
|
|
||||||
selector:
|
|
||||||
matchLabels:
|
|
||||||
app.kubernetes.io/instance: extra
|
|
||||||
app.kubernetes.io/name: postgresql
|
|
||||||
serviceName: "postgres"
|
|
||||||
replicas: 1
|
|
||||||
template:
|
|
||||||
metadata:
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/instance: extra
|
|
||||||
app.kubernetes.io/name: postgresql
|
|
||||||
spec:
|
|
||||||
terminationGracePeriodSeconds: 10
|
|
||||||
containers:
|
|
||||||
- name: pg
|
|
||||||
image: postgres:16-alpine
|
|
||||||
readinessProbe:
|
|
||||||
exec:
|
|
||||||
command: [ "pg_isready", "-U", "dinum", "-d", "meet", "-h", "127.0.0.1" ]
|
|
||||||
initialDelaySeconds: 5
|
|
||||||
periodSeconds: 5
|
|
||||||
livenessProbe:
|
|
||||||
exec:
|
|
||||||
command: [ "pg_isready", "-U", "dinum", "-d", "meet", "-h", "127.0.0.1" ]
|
|
||||||
initialDelaySeconds: 15
|
|
||||||
periodSeconds: 10
|
|
||||||
ports:
|
|
||||||
- containerPort: 5432
|
|
||||||
name: tcp-postgresql
|
|
||||||
env:
|
|
||||||
- name: POSTGRES_PASSWORD
|
|
||||||
value: pass
|
|
||||||
- name: POSTGRES_USER
|
|
||||||
value: dinum
|
|
||||||
- name: POSTGRES_DB
|
|
||||||
value: meet
|
|
||||||
volumeMounts:
|
|
||||||
- name: data
|
|
||||||
mountPath: /var/lib/postgresql/data
|
|
||||||
volumeClaimTemplates:
|
|
||||||
- metadata:
|
|
||||||
name: data
|
|
||||||
spec:
|
|
||||||
accessModes: [ "ReadWriteOnce" ]
|
|
||||||
resources:
|
|
||||||
requests:
|
|
||||||
storage: 1Gi
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
---
|
|
||||||
apiVersion: v1
|
|
||||||
kind: Service
|
|
||||||
metadata:
|
|
||||||
name: redis-master
|
|
||||||
namespace: {{ .Release.Namespace | quote }}
|
|
||||||
spec:
|
|
||||||
ports:
|
|
||||||
- name: tcp-redis
|
|
||||||
port: 6379
|
|
||||||
protocol: TCP
|
|
||||||
targetPort: tcp-redis
|
|
||||||
selector:
|
|
||||||
app.kubernetes.io/instance: extra
|
|
||||||
app.kubernetes.io/name: redis
|
|
||||||
type: ClusterIP
|
|
||||||
---
|
|
||||||
apiVersion: v1
|
|
||||||
kind: ConfigMap
|
|
||||||
metadata:
|
|
||||||
name: redis
|
|
||||||
data:
|
|
||||||
redis.conf: |
|
|
||||||
bind 0.0.0.0
|
|
||||||
port 6379
|
|
||||||
user default on >pass ~* &* +@all
|
|
||||||
---
|
|
||||||
apiVersion: apps/v1
|
|
||||||
kind: Deployment
|
|
||||||
metadata:
|
|
||||||
name: redis
|
|
||||||
namespace: {{ .Release.Namespace | quote }}
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/instance: extra
|
|
||||||
app.kubernetes.io/name: redis
|
|
||||||
spec:
|
|
||||||
selector:
|
|
||||||
matchLabels:
|
|
||||||
app.kubernetes.io/instance: extra
|
|
||||||
app.kubernetes.io/name: redis
|
|
||||||
replicas: 1
|
|
||||||
template:
|
|
||||||
metadata:
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/instance: extra
|
|
||||||
app.kubernetes.io/name: redis
|
|
||||||
spec:
|
|
||||||
containers:
|
|
||||||
- name: redis
|
|
||||||
args:
|
|
||||||
- redis-server
|
|
||||||
- /usr/local/etc/redis/redis.conf
|
|
||||||
image: "redis:8.2-alpine"
|
|
||||||
imagePullPolicy: IfNotPresent
|
|
||||||
ports:
|
|
||||||
- containerPort: 6379
|
|
||||||
name: tcp-redis
|
|
||||||
volumeMounts:
|
|
||||||
- name: redis
|
|
||||||
mountPath: "/usr/local/etc/redis"
|
|
||||||
readOnly: true
|
|
||||||
volumes:
|
|
||||||
- name: redis
|
|
||||||
configMap:
|
|
||||||
name: redis
|
|
||||||
@@ -13,6 +13,9 @@ repositories:
|
|||||||
- name: livekit
|
- name: livekit
|
||||||
url: https://helm.livekit.io
|
url: https://helm.livekit.io
|
||||||
|
|
||||||
|
- name: dev-backends
|
||||||
|
url: https://suitenumerique.github.io/helm-dev-backend
|
||||||
|
|
||||||
releases:
|
releases:
|
||||||
- name: extra
|
- name: extra
|
||||||
installed: {{ regexMatch "^dev.*" .Environment.Name }}
|
installed: {{ regexMatch "^dev.*" .Environment.Name }}
|
||||||
@@ -27,8 +30,59 @@ releases:
|
|||||||
- enablePermanentRedirect: {{ .Values | get "enablePermanentRedirect" "False"}}
|
- enablePermanentRedirect: {{ .Values | get "enablePermanentRedirect" "False"}}
|
||||||
- oldDomain: {{ .Values | get "oldDomain" "demo.com" }}
|
- oldDomain: {{ .Values | get "oldDomain" "demo.com" }}
|
||||||
- newDomain: {{ .Values | get "newDomain" "demo.com" }}
|
- newDomain: {{ .Values | get "newDomain" "demo.com" }}
|
||||||
- realm: |
|
|
||||||
{{ readFile "../../docker/auth/realm.json" | replace "http://localhost:3200" "https://meet.127.0.0.1.nip.io" | indent 8 }}
|
- name: dev-backend
|
||||||
|
installed: {{ regexMatch "^dev.*" .Environment.Name }}
|
||||||
|
namespace: {{ .Namespace }}
|
||||||
|
chart: dev-backends/dev-backend
|
||||||
|
version: 0.0.14
|
||||||
|
values:
|
||||||
|
- postgres:
|
||||||
|
enabled: true
|
||||||
|
username: dinum
|
||||||
|
password: pass
|
||||||
|
database: meet
|
||||||
|
size: 1Gi
|
||||||
|
- redis:
|
||||||
|
enabled: true
|
||||||
|
username: user
|
||||||
|
password: pass
|
||||||
|
- garage:
|
||||||
|
enabled: true
|
||||||
|
accessKey: meet-access-key
|
||||||
|
secretKey: meet-secret-access-key
|
||||||
|
bucket: meet-media-storage
|
||||||
|
region: local
|
||||||
|
persistence: false
|
||||||
|
ingress:
|
||||||
|
enabled: true
|
||||||
|
hostname: garage.127.0.0.1.nip.io
|
||||||
|
annotations:
|
||||||
|
nginx.ingress.kubernetes.io/proxy-body-size: 10m
|
||||||
|
tls:
|
||||||
|
enabled: true
|
||||||
|
secretName: meet-tls
|
||||||
|
cors:
|
||||||
|
allowedOrigins:
|
||||||
|
- https://meet.127.0.0.1.nip.io
|
||||||
|
- keycloak:
|
||||||
|
enabled: true
|
||||||
|
hostname: keycloak.127.0.0.1.nip.io
|
||||||
|
username: admin
|
||||||
|
password: admin
|
||||||
|
tls:
|
||||||
|
enabled: true
|
||||||
|
secretName: meet-tls
|
||||||
|
db:
|
||||||
|
username: dinum
|
||||||
|
password: pass
|
||||||
|
database: keycloak
|
||||||
|
size: 1Gi
|
||||||
|
realm:
|
||||||
|
name: meet
|
||||||
|
username: meet
|
||||||
|
password: meet
|
||||||
|
email: meet@example.com
|
||||||
|
|
||||||
|
|
||||||
- name: meet
|
- name: meet
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "mail_mjml",
|
"name": "mail_mjml",
|
||||||
"version": "1.33.0",
|
"version": "1.34.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "mail_mjml",
|
"name": "mail_mjml",
|
||||||
"version": "1.33.0",
|
"version": "1.34.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@html-to/text-cli": "0.6.1",
|
"@html-to/text-cli": "0.6.1",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "mail_mjml",
|
"name": "mail_mjml",
|
||||||
"version": "1.33.0",
|
"version": "1.34.0",
|
||||||
"description": "An util to generate html and text django's templates from mjml templates",
|
"description": "An util to generate html and text django's templates from mjml templates",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "sdk",
|
"name": "sdk",
|
||||||
"version": "1.33.0",
|
"version": "1.34.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "sdk",
|
"name": "sdk",
|
||||||
"version": "1.33.0",
|
"version": "1.34.0",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"./library",
|
"./library",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "sdk",
|
"name": "sdk",
|
||||||
"version": "1.33.0",
|
"version": "1.34.0",
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"description": "",
|
"description": "",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "summary"
|
name = "summary"
|
||||||
version = "1.33.0"
|
version = "1.34.0"
|
||||||
requires-python = ">=3.13"
|
requires-python = ">=3.13"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"fastapi[standard]>=0.105.0",
|
"fastapi[standard]>=0.105.0",
|
||||||
|
|||||||
Generated
+1
-1
@@ -1484,7 +1484,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "summary"
|
name = "summary"
|
||||||
version = "1.33.0"
|
version = "1.34.0"
|
||||||
source = { editable = "." }
|
source = { editable = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "boto3" },
|
{ name = "boto3" },
|
||||||
|
|||||||
Reference in New Issue
Block a user