Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c6de0041da | |||
| 0c98dee3ef | |||
| 768b983f8d | |||
| 3a63b1e410 | |||
| 65b194d432 |
@@ -43,12 +43,12 @@ jobs:
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USER }}
|
||||
password: ${{ secrets.DOCKER_HUB_PASSWORD }}
|
||||
# -
|
||||
# name: Run trivy scan
|
||||
# uses: numerique-gouv/action-trivy-cache@main
|
||||
# with:
|
||||
# docker-build-args: '--target backend-production -f Dockerfile'
|
||||
# docker-image-name: '${{ env.DOCKER_CONTAINER_REGISTRY_HOSTNAME }}/${{ env.DOCKER_CONTAINER_REGISTRY_NAMESPACE }}/meet-backend:${{ github.sha }}'
|
||||
-
|
||||
name: Run trivy scan
|
||||
uses: numerique-gouv/action-trivy-cache@main
|
||||
with:
|
||||
docker-build-args: '--target backend-production -f Dockerfile'
|
||||
docker-image-name: '${{ env.DOCKER_CONTAINER_REGISTRY_HOSTNAME }}/${{ env.DOCKER_CONTAINER_REGISTRY_NAMESPACE }}/meet-backend:${{ github.sha }}'
|
||||
-
|
||||
name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
@@ -86,12 +86,12 @@ jobs:
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USER }}
|
||||
password: ${{ secrets.DOCKER_HUB_PASSWORD }}
|
||||
# -
|
||||
# name: Run trivy scan
|
||||
# uses: numerique-gouv/action-trivy-cache@main
|
||||
# with:
|
||||
# docker-build-args: '-f src/frontend/Dockerfile --target frontend-production'
|
||||
# docker-image-name: '${{ env.DOCKER_CONTAINER_REGISTRY_HOSTNAME }}/${{ env.DOCKER_CONTAINER_REGISTRY_NAMESPACE }}/meet-frontend:${{ github.sha }}'
|
||||
-
|
||||
name: Run trivy scan
|
||||
uses: numerique-gouv/action-trivy-cache@main
|
||||
with:
|
||||
docker-build-args: '-f src/frontend/Dockerfile --target frontend-production'
|
||||
docker-image-name: '${{ env.DOCKER_CONTAINER_REGISTRY_HOSTNAME }}/${{ env.DOCKER_CONTAINER_REGISTRY_NAMESPACE }}/meet-frontend:${{ github.sha }}'
|
||||
-
|
||||
name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
@@ -130,12 +130,12 @@ jobs:
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USER }}
|
||||
password: ${{ secrets.DOCKER_HUB_PASSWORD }}
|
||||
# -
|
||||
# name: Run trivy scan
|
||||
# uses: numerique-gouv/action-trivy-cache@main
|
||||
# with:
|
||||
# docker-build-args: '-f docker/dinum-frontend/Dockerfile --target frontend-production'
|
||||
# docker-image-name: '${{ env.DOCKER_CONTAINER_REGISTRY_HOSTNAME }}/${{ env.DOCKER_CONTAINER_REGISTRY_NAMESPACE }}/meet-frontend-dinum:${{ github.sha }}'
|
||||
-
|
||||
name: Run trivy scan
|
||||
uses: numerique-gouv/action-trivy-cache@main
|
||||
with:
|
||||
docker-build-args: '-f docker/dinum-frontend/Dockerfile --target frontend-production'
|
||||
docker-image-name: '${{ env.DOCKER_CONTAINER_REGISTRY_HOSTNAME }}/${{ env.DOCKER_CONTAINER_REGISTRY_NAMESPACE }}/meet-frontend-dinum:${{ github.sha }}'
|
||||
-
|
||||
name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
@@ -174,13 +174,13 @@ jobs:
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USER }}
|
||||
password: ${{ secrets.DOCKER_HUB_PASSWORD }}
|
||||
# -
|
||||
# name: Run trivy scan
|
||||
# uses: numerique-gouv/action-trivy-cache@main
|
||||
# continue-on-error: true
|
||||
# with:
|
||||
# docker-build-args: '-f src/summary/Dockerfile --target production'
|
||||
# docker-image-name: '${{ env.DOCKER_CONTAINER_REGISTRY_HOSTNAME }}/${{ env.DOCKER_CONTAINER_REGISTRY_NAMESPACE }}/meet-summary:${{ github.sha }}'
|
||||
-
|
||||
name: Run trivy scan
|
||||
uses: numerique-gouv/action-trivy-cache@main
|
||||
continue-on-error: true
|
||||
with:
|
||||
docker-build-args: '-f src/summary/Dockerfile --target production'
|
||||
docker-image-name: '${{ env.DOCKER_CONTAINER_REGISTRY_HOSTNAME }}/${{ env.DOCKER_CONTAINER_REGISTRY_NAMESPACE }}/meet-summary:${{ github.sha }}'
|
||||
docker-context: './src/summary'
|
||||
-
|
||||
name: Build and push
|
||||
@@ -220,14 +220,14 @@ jobs:
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USER }}
|
||||
password: ${{ secrets.DOCKER_HUB_PASSWORD }}
|
||||
# -
|
||||
# name: Run trivy scan
|
||||
# uses: numerique-gouv/action-trivy-cache@main
|
||||
# continue-on-error: true
|
||||
# with:
|
||||
# docker-build-args: '-f src/agents/Dockerfile --target production'
|
||||
# docker-image-name: '${{ env.DOCKER_CONTAINER_REGISTRY_HOSTNAME }}/${{ env.DOCKER_CONTAINER_REGISTRY_NAMESPACE }}/meet-agents:${{ github.sha }}'
|
||||
# docker-context: './src/agents'
|
||||
-
|
||||
name: Run trivy scan
|
||||
uses: numerique-gouv/action-trivy-cache@main
|
||||
continue-on-error: true
|
||||
with:
|
||||
docker-build-args: '-f src/agents/Dockerfile --target production'
|
||||
docker-image-name: '${{ env.DOCKER_CONTAINER_REGISTRY_HOSTNAME }}/${{ env.DOCKER_CONTAINER_REGISTRY_NAMESPACE }}/meet-agents:${{ github.sha }}'
|
||||
docker-context: './src/agents'
|
||||
-
|
||||
name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
# /!\
|
||||
# Security Note: This action is not hardened against prompt injection attacks and should only be used
|
||||
# to review trusted PRs. Configure your repository with "Require approval for all external contributors"
|
||||
# to ensure workflows only run after a maintainer has reviewed the PR.
|
||||
name: Security Review
|
||||
|
||||
permissions:
|
||||
pull-requests: write # Needed for leaving PR comments
|
||||
contents: read
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- 'main'
|
||||
|
||||
jobs:
|
||||
security:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
fetch-depth: 2
|
||||
|
||||
- uses: anthropics/claude-code-security-review@0c6a49f1fa56a1d472575da86a94dbc1edb78eda
|
||||
with:
|
||||
comment-pr: true
|
||||
exclude-directories: docs,gitlint,LICENSES,bin
|
||||
claude-api-key: ${{ secrets.CLAUDE_API_KEY }}
|
||||
+1
-26
@@ -8,25 +8,9 @@ and this project adheres to
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Changed
|
||||
|
||||
- 🔒️(backend) enhance API input validation to strengthen security #1053
|
||||
- 🦺(backend) strengthen API validation for recording options #1063
|
||||
- ⚡️(frontend) optimize few performance caveats #1073
|
||||
- 🔒️(helm) introduce a dedicated Kubernetes Ingress for webhook-livekit #1066
|
||||
|
||||
### Fixed
|
||||
|
||||
- 🐛(migrations) use settings in migrations #1058
|
||||
- 💄(frontend) truncate pinned participant name with ellipsis on overflow #1056
|
||||
- ♿(frontend) prevent focus ring clipping on invite dialog #1078
|
||||
|
||||
## [1.9.0] - 2026-03-02
|
||||
|
||||
### Added
|
||||
|
||||
- 👷(docker) add arm64 platform support for image builds
|
||||
- ✨(summary) add localization support for transcription context text
|
||||
|
||||
### Changed
|
||||
|
||||
@@ -37,17 +21,8 @@ and this project adheres to
|
||||
- 🚚(frontend) rename "wellknown" directory to "well-known" #1009
|
||||
- 🌐(frontend) localize SR modifier labels #1010
|
||||
- ⬆️(backend) update python dependencies #1011
|
||||
- ♿️(frontend) fix focus ring on tab container components #1012
|
||||
- ♿️(frontend) upgrade join meeting modal accessibility #1027
|
||||
- ⬆️(python) bump minimal required python version to 3.13 #1033
|
||||
- ♿️(frontend) improve accessibility of the IntroSlider carousel #1026
|
||||
- ♿️(frontend) add skip link component for keyboard navigation #1019
|
||||
- ♿️(frontend) announce mic/camera state to SR on shortcut toggle #1052
|
||||
- ✨(frontend) add Ctrl+Shift+/ to open shortcuts settings #1050
|
||||
- ♿️(a11y) fix focus ring on tab container components
|
||||
|
||||
### Fixed
|
||||
|
||||
- 🩹(frontend) fix German language preference update #1021
|
||||
|
||||
## [1.8.0] - 2026-02-20
|
||||
|
||||
|
||||
@@ -23,10 +23,9 @@
|
||||
# ==============================================================================
|
||||
# VARIABLES
|
||||
|
||||
ESC := $(shell printf '\033')
|
||||
BOLD := $(ESC)[1m
|
||||
RESET := $(ESC)[0m
|
||||
GREEN := $(ESC)[1;32m
|
||||
BOLD := \033[1m
|
||||
RESET := \033[0m
|
||||
GREEN := \033[1;32m
|
||||
|
||||
|
||||
# -- Database
|
||||
@@ -86,8 +85,7 @@ bootstrap: \
|
||||
demo \
|
||||
back-i18n-compile \
|
||||
mails-install \
|
||||
mails-build \
|
||||
run
|
||||
mails-build
|
||||
.PHONY: bootstrap
|
||||
|
||||
# -- Docker/compose
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
Gitlint extra rule to validate that the message title is of the form
|
||||
"<gitmoji>(<scope>) <subject>"
|
||||
"""
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import re
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"plugins": [
|
||||
"office-addins"
|
||||
],
|
||||
"extends": [
|
||||
"plugin:office-addins/recommended"
|
||||
]
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 4.6 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.6 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.3 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.1 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 4.7 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"presets": [
|
||||
[
|
||||
"@babel/preset-env",
|
||||
{
|
||||
"targets": {
|
||||
"esmodules": false
|
||||
}
|
||||
}
|
||||
],
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<OfficeApp xmlns="http://schemas.microsoft.com/office/appforoffice/1.1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:bt="http://schemas.microsoft.com/office/officeappbasictypes/1.0" xmlns:mailappor="http://schemas.microsoft.com/office/mailappversionoverrides/1.0" xsi:type="MailApp">
|
||||
<Id>a025f0f6-757a-4790-97f3-99c66c4a5795</Id>
|
||||
<Version>0.0.1.0</Version>
|
||||
<ProviderName>Visio</ProviderName>
|
||||
<DefaultLocale>en-US</DefaultLocale>
|
||||
<DisplayName DefaultValue="Visio"/>
|
||||
<Description DefaultValue="Ajoutez facilement un lien de réunion Visio à vos emails et événements Outlook."/>
|
||||
<IconUrl DefaultValue="https://localhost:3000/assets/icon-64.png"/>
|
||||
<HighResolutionIconUrl DefaultValue="https://localhost:3000/assets/icon-128.png"/>
|
||||
<SupportUrl DefaultValue="https://www.contoso.com/help"/>
|
||||
<AppDomains>
|
||||
<AppDomain>https://localhost:3000</AppDomain>
|
||||
<AppDomain>https://meet.127.0.0.1.nip.io</AppDomain>
|
||||
</AppDomains>
|
||||
<Hosts>
|
||||
<Host Name="Mailbox"/>
|
||||
</Hosts>
|
||||
<Requirements>
|
||||
<Sets>
|
||||
<Set Name="Mailbox" MinVersion="1.1"/>
|
||||
</Sets>
|
||||
</Requirements>
|
||||
<FormSettings>
|
||||
<Form xsi:type="ItemRead">
|
||||
<DesktopSettings>
|
||||
<SourceLocation DefaultValue="https://localhost:3000/taskpane.html"/>
|
||||
<RequestedHeight>250</RequestedHeight>
|
||||
</DesktopSettings>
|
||||
</Form>
|
||||
<Form xsi:type="ItemEdit">
|
||||
<DesktopSettings>
|
||||
<SourceLocation DefaultValue="https://localhost:3000/taskpane.html"/>
|
||||
</DesktopSettings>
|
||||
</Form>
|
||||
</FormSettings>
|
||||
<Permissions>ReadWriteItem</Permissions>
|
||||
<Rule xsi:type="RuleCollection" Mode="Or">
|
||||
<Rule xsi:type="ItemIs" ItemType="Message" FormType="Read"/>
|
||||
<Rule xsi:type="ItemIs" ItemType="Message" FormType="Edit"/>
|
||||
<Rule xsi:type="ItemIs" ItemType="Appointment" FormType="Edit"/>
|
||||
</Rule>
|
||||
<DisableEntityHighlighting>false</DisableEntityHighlighting>
|
||||
<VersionOverrides xmlns="http://schemas.microsoft.com/office/mailappversionoverrides" xsi:type="VersionOverridesV1_0">
|
||||
<Requirements>
|
||||
<bt:Sets DefaultMinVersion="1.3">
|
||||
<bt:Set Name="Mailbox"/>
|
||||
</bt:Sets>
|
||||
</Requirements>
|
||||
<Hosts>
|
||||
<Host xsi:type="MailHost">
|
||||
<DesktopFormFactor>
|
||||
<FunctionFile resid="Commands.Url"/>
|
||||
|
||||
<!-- ─── Mail: Read ─────────────────────────────────────────── -->
|
||||
<ExtensionPoint xsi:type="MessageReadCommandSurface">
|
||||
<OfficeTab id="TabDefault">
|
||||
<Group id="msgReadGroup">
|
||||
<Label resid="GroupLabel"/>
|
||||
<Control xsi:type="Button" id="msgReadOpenPaneButton">
|
||||
<Label resid="TaskpaneButton.Label"/>
|
||||
<Supertip>
|
||||
<Title resid="TaskpaneButton.Label"/>
|
||||
<Description resid="TaskpaneButton.Tooltip"/>
|
||||
</Supertip>
|
||||
<Icon>
|
||||
<bt:Image size="16" resid="Icon.16x16"/>
|
||||
<bt:Image size="32" resid="Icon.32x32"/>
|
||||
<bt:Image size="80" resid="Icon.80x80"/>
|
||||
</Icon>
|
||||
<Action xsi:type="ShowTaskpane">
|
||||
<SourceLocation resid="Taskpane.Url"/>
|
||||
</Action>
|
||||
</Control>
|
||||
</Group>
|
||||
</OfficeTab>
|
||||
</ExtensionPoint>
|
||||
|
||||
<!-- ─── Mail: Compose ─────────────────────────────────────── -->
|
||||
<ExtensionPoint xsi:type="MessageComposeCommandSurface">
|
||||
<OfficeTab id="TabDefault">
|
||||
<Group id="msgComposeGroup">
|
||||
<Label resid="GroupLabel"/>
|
||||
<Control xsi:type="Button" id="msgComposeOpenPaneButton">
|
||||
<Label resid="TaskpaneButton.Label"/>
|
||||
<Supertip>
|
||||
<Title resid="TaskpaneButton.Label"/>
|
||||
<Description resid="TaskpaneButton.Tooltip"/>
|
||||
</Supertip>
|
||||
<Icon>
|
||||
<bt:Image size="16" resid="Icon.16x16"/>
|
||||
<bt:Image size="32" resid="Icon.32x32"/>
|
||||
<bt:Image size="80" resid="Icon.80x80"/>
|
||||
</Icon>
|
||||
<Action xsi:type="ShowTaskpane">
|
||||
<SourceLocation resid="Taskpane.Url"/>
|
||||
</Action>
|
||||
</Control>
|
||||
</Group>
|
||||
</OfficeTab>
|
||||
</ExtensionPoint>
|
||||
|
||||
<!-- ─── Calendar: Compose (New/Edit appointment) ──────────── -->
|
||||
<ExtensionPoint xsi:type="AppointmentOrganizerCommandSurface">
|
||||
<OfficeTab id="TabDefault">
|
||||
<Group id="apptComposeGroup">
|
||||
<Label resid="GroupLabel"/>
|
||||
|
||||
<!-- Button 1: Generate meeting link (function call) -->
|
||||
<Control xsi:type="Button" id="apptGenerateLinkButton">
|
||||
<Label resid="GenerateLink.Label"/>
|
||||
<Supertip>
|
||||
<Title resid="GenerateLink.Label"/>
|
||||
<Description resid="GenerateLink.Tooltip"/>
|
||||
</Supertip>
|
||||
<Icon>
|
||||
<bt:Image size="16" resid="Icon.16x16"/>
|
||||
<bt:Image size="32" resid="Icon.32x32"/>
|
||||
<bt:Image size="80" resid="Icon.80x80"/>
|
||||
</Icon>
|
||||
<Action xsi:type="ExecuteFunction">
|
||||
<FunctionName>generateMeetingLinkFromCalendar</FunctionName>
|
||||
</Action>
|
||||
</Control>
|
||||
|
||||
<!-- Button 2: Open settings taskpane -->
|
||||
<Control xsi:type="Button" id="apptOpenSettingsButton">
|
||||
<Label resid="OpenSettings.Label"/>
|
||||
<Supertip>
|
||||
<Title resid="OpenSettings.Label"/>
|
||||
<Description resid="OpenSettings.Tooltip"/>
|
||||
</Supertip>
|
||||
<Icon>
|
||||
<bt:Image size="16" resid="Icon.16x16"/>
|
||||
<bt:Image size="32" resid="Icon.32x32"/>
|
||||
<bt:Image size="80" resid="Icon.80x80"/>
|
||||
</Icon>
|
||||
<Action xsi:type="ShowTaskpane">
|
||||
<SourceLocation resid="Taskpane.Url"/>
|
||||
</Action>
|
||||
</Control>
|
||||
|
||||
</Group>
|
||||
</OfficeTab>
|
||||
</ExtensionPoint>
|
||||
|
||||
</DesktopFormFactor>
|
||||
</Host>
|
||||
</Hosts>
|
||||
<Resources>
|
||||
<bt:Images>
|
||||
<bt:Image id="Icon.16x16" DefaultValue="https://localhost:3000/assets/icon-16.png"/>
|
||||
<bt:Image id="Icon.32x32" DefaultValue="https://localhost:3000/assets/icon-32.png"/>
|
||||
<bt:Image id="Icon.80x80" DefaultValue="https://localhost:3000/assets/icon-80.png"/>
|
||||
</bt:Images>
|
||||
<bt:Urls>
|
||||
<bt:Url id="Commands.Url" DefaultValue="https://localhost:3000/commands.html"/>
|
||||
<bt:Url id="Taskpane.Url" DefaultValue="https://localhost:3000/taskpane.html"/>
|
||||
</bt:Urls>
|
||||
<bt:ShortStrings>
|
||||
<bt:String id="GroupLabel" DefaultValue="Visio"/>
|
||||
<bt:String id="TaskpaneButton.Label" DefaultValue="Ouvrir le panneau"/>
|
||||
<bt:String id="GenerateLink.Label" DefaultValue="Générer un lien de réunion"/>
|
||||
<bt:String id="OpenSettings.Label" DefaultValue="Paramètres"/>
|
||||
</bt:ShortStrings>
|
||||
<bt:LongStrings>
|
||||
<bt:String id="TaskpaneButton.Tooltip" DefaultValue="Ouvre le panneau de connexion Visio."/>
|
||||
<bt:String id="GenerateLink.Tooltip" DefaultValue="Génère un lien de réunion Visio et l'insère dans l'événement."/>
|
||||
<bt:String id="OpenSettings.Tooltip" DefaultValue="Ouvre les paramètres de connexion Visio."/>
|
||||
</bt:LongStrings>
|
||||
</Resources>
|
||||
</VersionOverrides>
|
||||
</OfficeApp>
|
||||
+16176
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,62 @@
|
||||
{
|
||||
"name": "office-addin-taskpane-js",
|
||||
"version": "0.0.1",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/OfficeDev/Office-Addin-TaskPane-JS.git"
|
||||
},
|
||||
"license": "MIT",
|
||||
"config": {
|
||||
"app_to_debug": "outlook",
|
||||
"app_type_to_debug": "desktop",
|
||||
"dev_server_port": 3000
|
||||
},
|
||||
"scripts": {
|
||||
"build": "webpack --mode production",
|
||||
"build:dev": "webpack --mode development",
|
||||
"dev-server": "webpack serve --mode development",
|
||||
"lint": "office-addin-lint check",
|
||||
"lint:fix": "office-addin-lint fix",
|
||||
"prettier": "office-addin-lint prettier",
|
||||
"signin": "office-addin-dev-settings m365-account login",
|
||||
"signout": "office-addin-dev-settings m365-account logout",
|
||||
"start": "office-addin-debugging start manifest.xml",
|
||||
"stop": "office-addin-debugging stop manifest.xml",
|
||||
"validate": "office-addin-manifest validate manifest.xml",
|
||||
"watch": "webpack --mode development --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"core-js": "^3.36.0",
|
||||
"regenerator-runtime": "^0.14.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.24.0",
|
||||
"@babel/preset-env": "^7.25.4",
|
||||
"@types/office-js": "^1.0.377",
|
||||
"@types/office-runtime": "^1.0.35",
|
||||
"acorn": "^8.11.3",
|
||||
"babel-loader": "^9.1.3",
|
||||
"copy-webpack-plugin": "^12.0.2",
|
||||
"eslint-plugin-office-addins": "^4.0.3",
|
||||
"file-loader": "^6.2.0",
|
||||
"html-loader": "^5.0.0",
|
||||
"html-webpack-plugin": "^5.6.0",
|
||||
"office-addin-cli": "^2.0.3",
|
||||
"office-addin-debugging": "^6.0.3",
|
||||
"office-addin-dev-certs": "^2.0.3",
|
||||
"office-addin-lint": "^3.0.3",
|
||||
"office-addin-manifest": "^2.0.3",
|
||||
"office-addin-prettier-config": "^2.0.1",
|
||||
"os-browserify": "^0.3.0",
|
||||
"process": "^0.11.10",
|
||||
"source-map-loader": "^5.0.0",
|
||||
"webpack": "^5.95.0",
|
||||
"webpack-cli": "^5.1.4",
|
||||
"webpack-dev-server": "5.1.0"
|
||||
},
|
||||
"prettier": "office-addin-prettier-config",
|
||||
"browserslist": [
|
||||
"last 2 versions",
|
||||
"ie 11"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=Edge" />
|
||||
<script src="https://appsforoffice.microsoft.com/lib/1/hosted/office.js"></script>
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
||||
@@ -0,0 +1,80 @@
|
||||
/* global Office */
|
||||
const { loadSession, buildMeetingMessage, BASE_URL } = require("../common");
|
||||
|
||||
Office.onReady(() => {});
|
||||
|
||||
function generateMeetingLinkFromCalendar(event) {
|
||||
const session = loadSession();
|
||||
|
||||
if (!session?.access_token) {
|
||||
Office.context.mailbox.item.notificationMessages.replaceAsync("meetNotif", {
|
||||
type: Office.MailboxEnums.ItemNotificationMessageType.ErrorMessage,
|
||||
message: "Vous n'êtes pas connecté. Ouvrez les paramètres pour vous connecter.",
|
||||
});
|
||||
event.completed();
|
||||
return;
|
||||
}
|
||||
|
||||
fetch(`${BASE_URL}/external-api/v1.0/rooms/`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": "Bearer " + session.access_token,
|
||||
},
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
console.log("Room created:", data);
|
||||
|
||||
const { url, message } = buildMeetingMessage(data);
|
||||
const item = Office.context.mailbox.item;
|
||||
|
||||
item.body.getAsync(Office.CoercionType.Html, (getResult) => {
|
||||
if (getResult.status !== Office.AsyncResultStatus.Succeeded) {
|
||||
item.notificationMessages.replaceAsync("meetNotif", {
|
||||
type: Office.MailboxEnums.ItemNotificationMessageType.ErrorMessage,
|
||||
message: `Erreur de lecture: ${getResult.error.message}`,
|
||||
});
|
||||
event.completed();
|
||||
return;
|
||||
}
|
||||
|
||||
item.body.setAsync(getResult.value + message, { coercionType: Office.CoercionType.Html }, (setResult) => {
|
||||
if (setResult.status !== Office.AsyncResultStatus.Succeeded) {
|
||||
item.notificationMessages.replaceAsync("meetNotif", {
|
||||
type: Office.MailboxEnums.ItemNotificationMessageType.ErrorMessage,
|
||||
message: `Erreur d'insertion: ${setResult.error.message}`,
|
||||
});
|
||||
event.completed();
|
||||
return;
|
||||
}
|
||||
|
||||
item.location.setAsync(url, (locationResult) => {
|
||||
if (locationResult.status === Office.AsyncResultStatus.Succeeded) {
|
||||
item.notificationMessages.replaceAsync("meetNotif", {
|
||||
type: Office.MailboxEnums.ItemNotificationMessageType.InformationalMessage,
|
||||
message: "Lien de réunion inséré !",
|
||||
icon: "Icon.80x80",
|
||||
persistent: false,
|
||||
});
|
||||
} else {
|
||||
item.notificationMessages.replaceAsync("meetNotif", {
|
||||
type: Office.MailboxEnums.ItemNotificationMessageType.ErrorMessage,
|
||||
message: `Erreur de localisation: ${locationResult.error.message}`,
|
||||
});
|
||||
}
|
||||
event.completed();
|
||||
});
|
||||
});
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
Office.context.mailbox.item.notificationMessages.replaceAsync("meetNotif", {
|
||||
type: Office.MailboxEnums.ItemNotificationMessageType.ErrorMessage,
|
||||
message: `Erreur: ${err.message}`,
|
||||
});
|
||||
event.completed();
|
||||
});
|
||||
}
|
||||
|
||||
Office.actions.associate("generateMeetingLinkFromCalendar", generateMeetingLinkFromCalendar);
|
||||
@@ -0,0 +1,99 @@
|
||||
/* global Office */
|
||||
|
||||
const BASE_URL = "https://meet.127.0.0.1.nip.io";
|
||||
|
||||
// ─── Session Storage ──────────────────────────────────────────────────────
|
||||
|
||||
function saveSession(data) {
|
||||
const expiresAt = data.expires_in
|
||||
? new Date(Date.now() + data.expires_in * 1000).toISOString()
|
||||
: null;
|
||||
|
||||
const payload = JSON.stringify({
|
||||
...data,
|
||||
expiresAt,
|
||||
savedAt: new Date().toISOString(),
|
||||
});
|
||||
|
||||
localStorage.setItem("meetSession", payload);
|
||||
|
||||
const rs = Office.context.roamingSettings;
|
||||
rs.set("meetSession", payload);
|
||||
rs.saveAsync((result) => {
|
||||
if (result.status !== Office.AsyncResultStatus.Succeeded) {
|
||||
console.error("RoamingSettings save failed:", result.error.message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function loadSession() {
|
||||
let session = null;
|
||||
|
||||
try {
|
||||
const stored = Office.context.roamingSettings.get("meetSession");
|
||||
if (stored) session = JSON.parse(stored);
|
||||
} catch (e) {
|
||||
console.warn("RoamingSettings read failed:", e);
|
||||
}
|
||||
|
||||
if (!session) {
|
||||
try {
|
||||
const stored = localStorage.getItem("meetSession");
|
||||
if (stored) session = JSON.parse(stored);
|
||||
} catch (e) {
|
||||
console.warn("localStorage read failed:", e);
|
||||
}
|
||||
}
|
||||
|
||||
if (!session) return null;
|
||||
|
||||
if (session.expiresAt && new Date() > new Date(session.expiresAt)) {
|
||||
console.warn("Token expired, clearing session.");
|
||||
clearSession();
|
||||
return null;
|
||||
}
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
function clearSession() {
|
||||
localStorage.removeItem("meetSession");
|
||||
try {
|
||||
const rs = Office.context.roamingSettings;
|
||||
rs.remove("meetSession");
|
||||
rs.saveAsync(() => console.log("RoamingSettings cleared."));
|
||||
} catch (e) {
|
||||
console.warn("Could not clear RoamingSettings:", e);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Meeting Message Builder ───────────────────────────────────────────────
|
||||
|
||||
function buildMeetingMessage(data) {
|
||||
const url = data.url;
|
||||
const phone = data.telephony?.phone_number;
|
||||
const pin = data.telephony?.pin_code;
|
||||
|
||||
const formattedPin = pin
|
||||
? pin.replace(/(\d{3})(\d{3})(\d{4})/, "$1 $2 $3") + "#"
|
||||
: "";
|
||||
|
||||
const formattedPhone = phone
|
||||
? phone.replace(/^\+33(\d)(\d{2})(\d{2})(\d{2})(\d{2})$/, "+33 $1 $2 $3 $4 $5")
|
||||
: phone;
|
||||
|
||||
const message = `<pre style="font-family:inherit; font-size:inherit; border:none; background:none; margin:16px 0;">
|
||||
────────────────────────────────────────
|
||||
Rejoindre la réunion LaSuite Meet
|
||||
|
||||
<a href="${url}">${url}</a>
|
||||
|
||||
Ou appelez (audio uniquement)
|
||||
(FR) ${formattedPhone}
|
||||
Code : ${formattedPin}
|
||||
────────────────────────────────────────</pre>`;
|
||||
|
||||
return { url, message };
|
||||
}
|
||||
|
||||
module.exports = { BASE_URL, saveSession, loadSession, clearSession, buildMeetingMessage };
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,58 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=Edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Visio</title>
|
||||
<link rel="stylesheet" href="taskpane.css" />
|
||||
<script src="https://appsforoffice.microsoft.com/lib/1/hosted/office.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="sideload-msg">Veuillez charger le complément.</div>
|
||||
|
||||
<div id="app-body">
|
||||
|
||||
<!-- Loading -->
|
||||
<div id="view-loading">
|
||||
<p class="intro-text">Chargement...</p>
|
||||
</div>
|
||||
|
||||
<!-- Unauthenticated -->
|
||||
<div id="view-unauth" style="display:none;">
|
||||
<p class="intro-text">
|
||||
<span>Ajoutez facilement un lien de réunion Visio à vos événements Outlook.</span>
|
||||
<a href="https://meet.numerique.gouv.fr" target="_blank" class="learn-more">En savoir plus</a>
|
||||
</p>
|
||||
|
||||
<hr class="divider" />
|
||||
|
||||
<button class="proconnect-button" id="btn-connect">
|
||||
<span class="proconnect-sr-only">S'identifier avec ProConnect</span>
|
||||
</button>
|
||||
|
||||
<p>
|
||||
<a
|
||||
href="https://www.proconnect.gouv.fr/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
title="Qu’est-ce que ProConnect ? - nouvelle fenêtre"
|
||||
>
|
||||
Qu’est-ce que ProConnect ?
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Authenticated -->
|
||||
<div id="view-auth" style="display:none;">
|
||||
<div id="btn-container">
|
||||
<button id="btn-generate">Ajouter une réunion Visio</button>
|
||||
<button id="btn-disconnect">Se déconnecter</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p id="status"></p>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,196 @@
|
||||
|
||||
const { BASE_URL, loadSession, saveSession, clearSession, buildMeetingMessage } = require("../common");
|
||||
|
||||
// ─── Views ────────────────────────────────────────────────────────────────
|
||||
|
||||
function showView(name) {
|
||||
document.getElementById("view-loading").style.display = "none";
|
||||
document.getElementById("view-unauth").style.display = "none";
|
||||
document.getElementById("view-auth").style.display = "none";
|
||||
document.getElementById(`view-${name}`).style.display = "block";
|
||||
}
|
||||
|
||||
function setStatus(msg) {
|
||||
document.getElementById("status").textContent = msg;
|
||||
}
|
||||
|
||||
// ─── Auth Flow ────────────────────────────────────────────────────────────
|
||||
|
||||
function connect() {
|
||||
setStatus("Démarrage de la session...");
|
||||
|
||||
fetch(`${BASE_URL}/api/v1.0/addons/sessions/`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
const session_id = data.session_id;
|
||||
setStatus("En attente d'authentification...");
|
||||
|
||||
let dialog = null;
|
||||
let pollInterval = null;
|
||||
let pollCount = 0;
|
||||
|
||||
pollInterval = setInterval(() => {
|
||||
|
||||
// ─── Timeout after 3 minutes ──────────────────────────────
|
||||
if (pollCount++ > 180) {
|
||||
clearInterval(pollInterval);
|
||||
if (dialog) dialog.close();
|
||||
setStatus("Délai d'authentification dépassé. Veuillez réessayer.");
|
||||
showView("unauth");
|
||||
return;
|
||||
}
|
||||
|
||||
fetch(`${BASE_URL}/api/v1.0/addons/sessions/${session_id}`, {
|
||||
method: "GET",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((sessionData) => {
|
||||
console.log("Polling:", sessionData);
|
||||
if (sessionData.state === "authenticated" && sessionData.access_token) {
|
||||
clearInterval(pollInterval);
|
||||
// if (dialog) dialog.close();
|
||||
saveSession(sessionData);
|
||||
setStatus("Connecté !");
|
||||
showView("auth");
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
clearInterval(pollInterval);
|
||||
setStatus(`Erreur de polling: ${err.message}`);
|
||||
});
|
||||
}, 1000);
|
||||
|
||||
// ─── Open transit dialog ──────────────────────────────────────
|
||||
const meetUrl = `${BASE_URL}/addons/transit/?session_id=${session_id}`;
|
||||
Office.context.ui.displayDialogAsync(
|
||||
meetUrl,
|
||||
{ height: 60, width: 50, displayInIframe: false },
|
||||
(asyncResult) => {
|
||||
if (asyncResult.status === Office.AsyncResultStatus.Failed) {
|
||||
clearInterval(pollInterval);
|
||||
setStatus(`Erreur dialog: ${asyncResult.error.message}`);
|
||||
return;
|
||||
}
|
||||
|
||||
dialog = asyncResult.value;
|
||||
|
||||
dialog.addEventHandler(Office.EventType.DialogMessageReceived, () => {
|
||||
clearInterval(pollInterval);
|
||||
dialog.close();
|
||||
});
|
||||
|
||||
dialog.addEventHandler(Office.EventType.DialogEventReceived, (arg) => {
|
||||
if (arg.error === 12006) {
|
||||
setStatus("Dialog fermé. En attente d'authentification...");
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
})
|
||||
.catch((err) => {
|
||||
setStatus(`Erreur de connexion: ${err.message}`);
|
||||
});
|
||||
}
|
||||
|
||||
function disconnect() {
|
||||
clearSession();
|
||||
setStatus("Déconnecté.");
|
||||
showView("unauth");
|
||||
}
|
||||
|
||||
function generateMeetingLink() {
|
||||
const session = loadSession();
|
||||
if (!session?.access_token) {
|
||||
setStatus("Session introuvable. Veuillez vous reconnecter.");
|
||||
showView("unauth");
|
||||
return;
|
||||
}
|
||||
|
||||
const btn = document.getElementById("btn-generate");
|
||||
btn.disabled = true;
|
||||
btn.textContent = "Génération...";
|
||||
|
||||
fetch(`${BASE_URL}/external-api/v1.0/rooms/`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": "Bearer " + session.access_token,
|
||||
},
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
console.log("Room created:", data);
|
||||
|
||||
const { url, message } = buildMeetingMessage(data);
|
||||
const item = Office.context.mailbox.item;
|
||||
|
||||
item.body.getAsync(Office.CoercionType.Html, (getResult) => {
|
||||
if (getResult.status !== Office.AsyncResultStatus.Succeeded) {
|
||||
setStatus(`Erreur de lecture: ${getResult.error.message}`);
|
||||
btn.disabled = false;
|
||||
btn.textContent = "Ajouter une réunion Visio";
|
||||
return;
|
||||
}
|
||||
|
||||
item.body.setAsync(
|
||||
getResult.value + message,
|
||||
{ coercionType: Office.CoercionType.Html },
|
||||
(setResult) => {
|
||||
if (setResult.status !== Office.AsyncResultStatus.Succeeded) {
|
||||
setStatus(`Erreur d'insertion: ${setResult.error.message}`);
|
||||
btn.disabled = false;
|
||||
btn.textContent = "Ajouter une réunion Visio";
|
||||
return;
|
||||
}
|
||||
|
||||
// ─── If calendar event, also set location ──────────────
|
||||
if (item.itemType === Office.MailboxEnums.ItemType.Appointment) {
|
||||
item.location.setAsync(url, (locationResult) => {
|
||||
btn.disabled = false;
|
||||
btn.textContent = "Ajouter une réunion Visio";
|
||||
if (locationResult.status === Office.AsyncResultStatus.Succeeded) {
|
||||
setStatus("Lien de réunion inséré !");
|
||||
} else {
|
||||
setStatus(`Erreur de localisation: ${locationResult.error.message}`);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
btn.disabled = false;
|
||||
btn.textContent = "Ajouter une réunion Visio";
|
||||
setStatus("Lien de réunion inséré !");
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
btn.disabled = false;
|
||||
btn.textContent = "Ajouter une réunion Visio";
|
||||
setStatus(`Erreur: ${err.message}`);
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Init ─────────────────────────────────────────────────────────────────
|
||||
|
||||
Office.onReady((info) => {
|
||||
if (info.host === Office.HostType.Outlook) {
|
||||
document.getElementById("sideload-msg").style.display = "none";
|
||||
document.getElementById("app-body").style.display = "flex";
|
||||
|
||||
document.getElementById("btn-connect").onclick = connect;
|
||||
document.getElementById("btn-disconnect").onclick = disconnect;
|
||||
document.getElementById("btn-generate").onclick = generateMeetingLink;
|
||||
|
||||
const session = loadSession();
|
||||
if (session?.state === "authenticated" && session?.access_token) {
|
||||
setStatus("Connecté.");
|
||||
showView("auth");
|
||||
} else {
|
||||
showView("unauth");
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,97 @@
|
||||
/* eslint-disable no-undef */
|
||||
|
||||
const devCerts = require("office-addin-dev-certs");
|
||||
const CopyWebpackPlugin = require("copy-webpack-plugin");
|
||||
const HtmlWebpackPlugin = require("html-webpack-plugin");
|
||||
|
||||
const urlDev = "https://localhost:3000/";
|
||||
const urlProd = "https://meet.127.0.0.1.nip.io/outlook-addin/";
|
||||
|
||||
async function getHttpsOptions() {
|
||||
const httpsOptions = await devCerts.getHttpsServerOptions();
|
||||
return { ca: httpsOptions.ca, key: httpsOptions.key, cert: httpsOptions.cert };
|
||||
}
|
||||
|
||||
module.exports = async (env, options) => {
|
||||
const dev = options.mode === "development";
|
||||
const config = {
|
||||
devtool: "source-map",
|
||||
entry: {
|
||||
polyfill: ["core-js/stable", "regenerator-runtime/runtime"],
|
||||
taskpane: ["./src/taskpane/taskpane.js", "./src/taskpane/taskpane.html"],
|
||||
commands: "./src/commands/commands.js",
|
||||
},
|
||||
output: {
|
||||
clean: true,
|
||||
},
|
||||
resolve: {
|
||||
extensions: [".html", ".js"],
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.js$/,
|
||||
exclude: /node_modules/,
|
||||
use: {
|
||||
loader: "babel-loader",
|
||||
},
|
||||
},
|
||||
{
|
||||
test: /\.html$/,
|
||||
exclude: /node_modules/,
|
||||
use: "html-loader",
|
||||
},
|
||||
{
|
||||
test: /\.(png|jpg|jpeg|gif|ico)$/,
|
||||
type: "asset/resource",
|
||||
generator: {
|
||||
filename: "assets/[name][ext][query]",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
plugins: [
|
||||
new HtmlWebpackPlugin({
|
||||
filename: "taskpane.html",
|
||||
template: "./src/taskpane/taskpane.html",
|
||||
chunks: ["polyfill", "taskpane"],
|
||||
}),
|
||||
new CopyWebpackPlugin({
|
||||
patterns: [
|
||||
{
|
||||
from: "assets/*",
|
||||
to: "assets/[name][ext][query]",
|
||||
},
|
||||
{
|
||||
from: "manifest*.xml",
|
||||
to: "[name]" + "[ext]",
|
||||
transform(content) {
|
||||
if (dev) {
|
||||
return content;
|
||||
} else {
|
||||
return content.toString().replace(new RegExp(urlDev, "g"), urlProd);
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
filename: "commands.html",
|
||||
template: "./src/commands/commands.html",
|
||||
chunks: ["polyfill", "commands"],
|
||||
}),
|
||||
],
|
||||
devServer: {
|
||||
headers: {
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
},
|
||||
server: {
|
||||
type: "https",
|
||||
options: env.WEBPACK_BUILD || options.https !== undefined ? options.https : await getHttpsOptions(),
|
||||
},
|
||||
port: process.env.npm_package_config_dev_server_port || 3000,
|
||||
},
|
||||
};
|
||||
|
||||
return config;
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
|
||||
[project]
|
||||
name = "agents"
|
||||
version = "1.9.0"
|
||||
version = "1.8.0"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = [
|
||||
"livekit-agents==1.3.10",
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
"""Meet core add-ons module."""
|
||||
@@ -0,0 +1,124 @@
|
||||
"""Authentication session management for add-ons using temporary cache-based sessions."""
|
||||
|
||||
import secrets
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from enum import Enum
|
||||
from logging import getLogger
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.cache import cache
|
||||
from django.core.exceptions import SuspiciousOperation
|
||||
|
||||
from core.models import User
|
||||
from core.services.jwt_token import JwtTokenService
|
||||
|
||||
logger = getLogger(__name__)
|
||||
|
||||
|
||||
class SessionState(str, Enum):
|
||||
"""Add-on authentication session states."""
|
||||
|
||||
PENDING = "pending"
|
||||
AUTHENTICATED = "authenticated"
|
||||
|
||||
|
||||
class TokenExchangeService:
|
||||
"""Manage temporary authentication sessions for add-on JWT token exchange."""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the service with the configured token service."""
|
||||
|
||||
self._token_service = JwtTokenService(
|
||||
secret_key=settings.ADDONS_JWT_SECRET_KEY,
|
||||
algorithm=settings.ADDONS_JWT_ALG,
|
||||
issuer=settings.ADDONS_JWT_ISSUER,
|
||||
audience=settings.ADDONS_JWT_AUDIENCE,
|
||||
expiration_seconds=settings.ADDONS_JWT_EXPIRATION_SECONDS,
|
||||
token_type=settings.ADDONS_JWT_TOKEN_TYPE,
|
||||
)
|
||||
|
||||
def _get_cache_key(self, session_id: str) -> str:
|
||||
"""Generate cache key for a session ID."""
|
||||
return f"{settings.ADDONS_SESSION_KEY_PREFIX}_{session_id}"
|
||||
|
||||
def init_session(self) -> str:
|
||||
"""Create a new pending authentication session and return its ID."""
|
||||
|
||||
session_id = secrets.token_urlsafe(settings.ADDONS_SESSION_ID_LENGTH)
|
||||
expires_at = datetime.now(timezone.utc) + timedelta(
|
||||
seconds=settings.ADDONS_SESSION_TIMEOUT
|
||||
)
|
||||
|
||||
session_data = {
|
||||
"state": SessionState.PENDING,
|
||||
"expires_at": expires_at.isoformat(),
|
||||
}
|
||||
|
||||
cache_key = self._get_cache_key(session_id)
|
||||
cache.set(
|
||||
cache_key,
|
||||
session_data,
|
||||
timeout=settings.ADDONS_SESSION_TIMEOUT,
|
||||
)
|
||||
|
||||
return session_id
|
||||
|
||||
def get_session(self, session_id: str) -> dict:
|
||||
"""Retrieve session data and clear it if authenticated."""
|
||||
|
||||
cache_key = self._get_cache_key(session_id)
|
||||
data = cache.get(cache_key)
|
||||
|
||||
if not data:
|
||||
return {}
|
||||
|
||||
if data.get("state") == SessionState.AUTHENTICATED:
|
||||
self.clear_session(session_id)
|
||||
|
||||
# Return copy without internal fields
|
||||
internal_fields = {"expires_at"}
|
||||
return {k: v for k, v in data.items() if k not in internal_fields}
|
||||
|
||||
def clear_session(self, session_id: str) -> None:
|
||||
"""Remove session data from cache."""
|
||||
|
||||
cache_key = self._get_cache_key(session_id)
|
||||
cache.delete(cache_key)
|
||||
|
||||
def set_access_token(self, user: User, session_id: str):
|
||||
"""Generate and store access token for an authenticated user session."""
|
||||
|
||||
cache_key = self._get_cache_key(session_id)
|
||||
existing_data = cache.get(cache_key)
|
||||
|
||||
if not existing_data:
|
||||
raise SuspiciousOperation("Session not found.")
|
||||
|
||||
expires_at = existing_data.get("expires_at", None)
|
||||
|
||||
if not expires_at:
|
||||
self.clear_session(session_id)
|
||||
raise SuspiciousOperation("Invalid session data.")
|
||||
|
||||
remaining_seconds = int(
|
||||
(
|
||||
datetime.fromisoformat(expires_at) - datetime.now(timezone.utc)
|
||||
).total_seconds()
|
||||
)
|
||||
|
||||
if remaining_seconds <= 0:
|
||||
self.clear_session(session_id)
|
||||
raise SuspiciousOperation("Session expired.")
|
||||
|
||||
if existing_data.get("state") != SessionState.PENDING:
|
||||
self.clear_session(session_id)
|
||||
raise SuspiciousOperation("Access token already set.")
|
||||
|
||||
response = self._token_service.generate_jwt(user, settings.ADDONS_SCOPES)
|
||||
new_data = {
|
||||
**existing_data,
|
||||
**response,
|
||||
"state": SessionState.AUTHENTICATED,
|
||||
}
|
||||
|
||||
cache.set(cache_key, new_data, timeout=remaining_seconds)
|
||||
@@ -0,0 +1,57 @@
|
||||
"""Add-ons views."""
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import SuspiciousOperation
|
||||
from django.shortcuts import redirect, render
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views.decorators.http import require_http_methods
|
||||
|
||||
from core.addons.service import SessionState, TokenExchangeService
|
||||
|
||||
|
||||
def render_error(request, message, status=400):
|
||||
"""Render simple error page."""
|
||||
return render(request, "addons/error.html", {"message": message}, status=status)
|
||||
|
||||
|
||||
@require_http_methods(["GET"])
|
||||
def transit_page(request):
|
||||
"""Initialize authentication flow for add-on session."""
|
||||
|
||||
session_id = request.GET.get("session_id")
|
||||
|
||||
if not session_id:
|
||||
return render_error(request, _("Session ID is required."), status=400)
|
||||
|
||||
data = TokenExchangeService().get_session(session_id)
|
||||
|
||||
if not data:
|
||||
return render_error(request, _("Session not found or expired."), status=404)
|
||||
|
||||
if data.get("state") != SessionState.PENDING:
|
||||
return render_error(request, _("Invalid session state."), status=400)
|
||||
|
||||
request.session[settings.ADDONS_SESSION_KEY_AUTH] = session_id
|
||||
|
||||
return_to = f"{settings.APPLICATION_BASE_URL}/addons/redirect"
|
||||
return redirect(f"/api/{settings.API_VERSION}/authenticate/?returnTo={return_to}")
|
||||
|
||||
|
||||
@require_http_methods(["GET"])
|
||||
def redirect_page(request):
|
||||
"""Complete authentication and close the popup window."""
|
||||
|
||||
if not request.user.is_authenticated:
|
||||
return render_error(request, _("Authentication required."), status=401)
|
||||
|
||||
session_id = request.session.pop(settings.ADDONS_SESSION_KEY_AUTH, None)
|
||||
|
||||
if not session_id:
|
||||
return render_error(request, _("No active session found."), status=404)
|
||||
|
||||
try:
|
||||
TokenExchangeService().set_access_token(request.user, session_id)
|
||||
except SuspiciousOperation:
|
||||
return render_error(request, _("Invalid or expired session."), status=400)
|
||||
|
||||
return render(request, "addons/redirect_success.html")
|
||||
@@ -0,0 +1,47 @@
|
||||
"""Add-ons API endpoints"""
|
||||
|
||||
from logging import getLogger
|
||||
|
||||
from rest_framework import (
|
||||
response as drf_response,
|
||||
)
|
||||
from rest_framework import status as drf_status
|
||||
from rest_framework import viewsets
|
||||
|
||||
from core.addons.service import TokenExchangeService
|
||||
|
||||
logger = getLogger(__name__)
|
||||
|
||||
|
||||
class AuthSessionViewSet(viewsets.ViewSet):
|
||||
"""ViewSet for managing add-on authentication sessions via token exchange."""
|
||||
|
||||
authentication_classes = []
|
||||
permission_classes = []
|
||||
throttle_classes = []
|
||||
|
||||
def create(self, request):
|
||||
"""Create a new pending authentication session."""
|
||||
session_id = TokenExchangeService().init_session()
|
||||
return drf_response.Response(
|
||||
{"session_id": session_id}, status=drf_status.HTTP_201_CREATED
|
||||
)
|
||||
|
||||
def retrieve(self, request, pk=None):
|
||||
"""Retrieve authentication session data by session ID."""
|
||||
data = TokenExchangeService().get_session(pk)
|
||||
|
||||
if not data:
|
||||
return drf_response.Response(
|
||||
{"detail": "Session not found or expired."},
|
||||
status=drf_status.HTTP_404_NOT_FOUND,
|
||||
)
|
||||
|
||||
return drf_response.Response(data, status=drf_status.HTTP_200_OK)
|
||||
|
||||
def destroy(self, request, pk=None):
|
||||
"""Delete an authentication session by session ID."""
|
||||
TokenExchangeService().clear_session(pk)
|
||||
return drf_response.Response(
|
||||
{"status": "ok"}, status=drf_status.HTTP_204_NO_CONTENT
|
||||
)
|
||||
@@ -1,14 +1,10 @@
|
||||
"""Client serializers for the Meet core app."""
|
||||
|
||||
# pylint: disable=abstract-method,no-name-in-module
|
||||
from typing import Literal
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import SuspiciousOperation
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from django_pydantic_field.rest_framework import SchemaField
|
||||
from pydantic import BaseModel, Field
|
||||
from livekit.api import ParticipantPermission
|
||||
from rest_framework import serializers
|
||||
from rest_framework.exceptions import PermissionDenied
|
||||
from timezone_field.rest_framework import TimeZoneSerializerField
|
||||
@@ -205,27 +201,6 @@ class BaseValidationOnlySerializer(serializers.Serializer):
|
||||
raise NotImplementedError(f"{self.__class__.__name__} is validation-only")
|
||||
|
||||
|
||||
class RecordingOptions(BaseModel):
|
||||
"""Configuration options for recording.
|
||||
|
||||
Attributes:
|
||||
language: ISO 639-1 language code compatible with whisperX.
|
||||
When `None`, the transcription engine will attempt to
|
||||
auto-detect the spoken language.
|
||||
transcribe: Whether to transcribe the recorded audio.
|
||||
When `None`, falls back to the application default.
|
||||
original_mode: The original recording mode before any override.
|
||||
Must be one of the valid RecordingModeChoices values when provided.
|
||||
|
||||
"""
|
||||
|
||||
language: str | None = None
|
||||
transcribe: bool | None = None
|
||||
original_mode: Literal["screen_recording", "transcript"] | None = None
|
||||
|
||||
model_config = {"extra": "forbid"}
|
||||
|
||||
|
||||
class StartRecordingSerializer(BaseValidationOnlySerializer):
|
||||
"""Validate start recording requests."""
|
||||
|
||||
@@ -238,11 +213,10 @@ class StartRecordingSerializer(BaseValidationOnlySerializer):
|
||||
"screen_recording or transcript.",
|
||||
},
|
||||
)
|
||||
options = SchemaField(
|
||||
schema=RecordingOptions | None,
|
||||
options = serializers.JSONField(
|
||||
required=False,
|
||||
allow_null=True,
|
||||
help_text="Recording options",
|
||||
default=dict,
|
||||
)
|
||||
|
||||
|
||||
@@ -287,28 +261,6 @@ class MuteParticipantSerializer(BaseParticipantsManagementSerializer):
|
||||
)
|
||||
|
||||
|
||||
class ParticipantPermission(BaseModel):
|
||||
"""Mirror the LiveKit ParticipantPermission protobuf.
|
||||
|
||||
Control what a participant is allowed to publish, subscribe, and do within a room.
|
||||
Unknown fields are rejected.
|
||||
"""
|
||||
|
||||
can_subscribe: bool | None = None
|
||||
can_publish: bool | None = None
|
||||
can_publish_data: bool | None = None
|
||||
can_publish_sources: list[int] = Field(
|
||||
default_factory=list
|
||||
) # TrackSource enum values
|
||||
hidden: bool | None = None
|
||||
recorder: bool | None = None
|
||||
can_update_metadata: bool | None = None
|
||||
agent: bool | None = None
|
||||
can_subscribe_metrics: bool | None = None
|
||||
|
||||
model_config = {"extra": "forbid"}
|
||||
|
||||
|
||||
class UpdateParticipantSerializer(BaseParticipantsManagementSerializer):
|
||||
"""Validate participant update data."""
|
||||
|
||||
@@ -320,11 +272,10 @@ class UpdateParticipantSerializer(BaseParticipantsManagementSerializer):
|
||||
allow_null=True,
|
||||
help_text="Participant attributes as JSON object",
|
||||
)
|
||||
permission = SchemaField(
|
||||
schema=ParticipantPermission | None,
|
||||
permission = serializers.DictField(
|
||||
required=False,
|
||||
allow_null=True,
|
||||
help_text="Participant permissions",
|
||||
help_text="Participant permission as JSON object",
|
||||
)
|
||||
name = serializers.CharField(
|
||||
max_length=255,
|
||||
@@ -334,33 +285,6 @@ class UpdateParticipantSerializer(BaseParticipantsManagementSerializer):
|
||||
help_text="Display name for the participant",
|
||||
)
|
||||
|
||||
def validate_permission(self, permission):
|
||||
"""Validate that the given permission does not include forbidden or unimplemented fields."""
|
||||
|
||||
if permission is None:
|
||||
return None
|
||||
|
||||
suspicious_fields = [
|
||||
field
|
||||
for field in settings.PARTICIPANT_FORBIDDEN_PERMISSION_FIELDS
|
||||
if getattr(permission, field) is not None
|
||||
]
|
||||
if suspicious_fields:
|
||||
raise SuspiciousOperation(
|
||||
f"Setting the following participant permissions is not allowed: "
|
||||
f"{', '.join(suspicious_fields)}."
|
||||
)
|
||||
if permission.can_subscribe_metrics is not None:
|
||||
raise serializers.ValidationError(
|
||||
{
|
||||
"permission": {
|
||||
"can_subscribe_metrics": "This permission is not implemented."
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
return permission
|
||||
|
||||
def validate(self, attrs):
|
||||
"""Ensure at least one update field is provided."""
|
||||
update_fields = ["metadata", "attributes", "permission", "name"]
|
||||
@@ -376,4 +300,12 @@ class UpdateParticipantSerializer(BaseParticipantsManagementSerializer):
|
||||
f"{', '.join(update_fields)}."
|
||||
)
|
||||
|
||||
if "permission" in attrs:
|
||||
try:
|
||||
ParticipantPermission(**attrs["permission"])
|
||||
except ValueError as e:
|
||||
raise serializers.ValidationError(
|
||||
{"permission": f"Invalid permission: {str(e)}"}
|
||||
) from e
|
||||
|
||||
return attrs
|
||||
|
||||
@@ -296,14 +296,12 @@ class RoomViewSet(
|
||||
)
|
||||
|
||||
mode = serializer.validated_data["mode"]
|
||||
options = serializer.validated_data.get("options")
|
||||
options = serializer.validated_data["options"]
|
||||
room = self.get_object()
|
||||
|
||||
# May raise exception if an active or initiated recording already exist for the room
|
||||
recording = models.Recording.objects.create(
|
||||
room=room,
|
||||
mode=mode,
|
||||
options=options.model_dump(exclude_none=True) if options else {},
|
||||
room=room, mode=mode, options=options
|
||||
)
|
||||
|
||||
models.RecordingAccess.objects.create(
|
||||
@@ -609,15 +607,13 @@ class RoomViewSet(
|
||||
serializer = serializers.UpdateParticipantSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
permission = serializer.validated_data.get("permission")
|
||||
|
||||
try:
|
||||
ParticipantsManagement().update(
|
||||
room_name=str(room.pk),
|
||||
identity=str(serializer.validated_data["participant_identity"]),
|
||||
metadata=serializer.validated_data.get("metadata"),
|
||||
attributes=serializer.validated_data.get("attributes"),
|
||||
permission=permission.model_dump() if permission else None,
|
||||
permission=serializer.validated_data.get("permission"),
|
||||
name=serializer.validated_data.get("name"),
|
||||
)
|
||||
except ParticipantsManagementException:
|
||||
|
||||
@@ -214,6 +214,25 @@ class ApplicationJWTAuthentication(BaseJWTAuthentication):
|
||||
raise exceptions.AuthenticationFailed("Invalid token type.")
|
||||
|
||||
|
||||
class AddonsJWTAuthentication(BaseJWTAuthentication):
|
||||
"""JWT authentication for addons API access.
|
||||
|
||||
Validates JWT tokens issued by addons for authenticating users.
|
||||
Tokens must include user_id to identify the authenticated user.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize authentication backend with application JWT settings from Django settings."""
|
||||
super().__init__(
|
||||
secret_key=settings.ADDONS_JWT_SECRET_KEY,
|
||||
algorithm=settings.ADDONS_JWT_ALG,
|
||||
issuer=settings.ADDONS_JWT_ISSUER,
|
||||
audience=settings.ADDONS_JWT_AUDIENCE,
|
||||
expiration_seconds=settings.ADDONS_JWT_EXPIRATION_SECONDS,
|
||||
token_type=settings.ADDONS_JWT_TOKEN_TYPE,
|
||||
)
|
||||
|
||||
|
||||
class ResourceServerBackend(LaSuiteBackend):
|
||||
"""OIDC Resource Server backend for user creation and retrieval."""
|
||||
|
||||
|
||||
@@ -173,6 +173,7 @@ class RoomViewSet(
|
||||
|
||||
authentication_classes = [
|
||||
authentication.ApplicationJWTAuthentication,
|
||||
authentication.AddonsJWTAuthentication,
|
||||
ResourceServerAuthentication,
|
||||
]
|
||||
permission_classes = [
|
||||
|
||||
@@ -44,7 +44,7 @@ class Migration(migrations.Migration):
|
||||
('sub', models.CharField(blank=True, help_text='Optional for pending users; required upon account activation. 255 characters or fewer. Letters, numbers, and @/./+/-/_ characters only.', max_length=255, null=True, unique=True, validators=[django.core.validators.RegexValidator(message='Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/_ characters.', regex='^[\\w.@+-]+\\Z')], verbose_name='sub')),
|
||||
('email', models.EmailField(blank=True, max_length=254, null=True, verbose_name='identity email address')),
|
||||
('admin_email', models.EmailField(blank=True, max_length=254, null=True, unique=True, verbose_name='admin email address')),
|
||||
('language', models.CharField(choices=settings.LANGUAGES, default=settings.LANGUAGE_CODE, help_text='The language in which the user wants to see the interface.', max_length=10, verbose_name='language')),
|
||||
('language', models.CharField(choices="(('en-us', 'English'), ('fr-fr', 'French'))", default='en-us', help_text='The language in which the user wants to see the interface.', max_length=10, verbose_name='language')),
|
||||
('timezone', timezone_field.fields.TimeZoneField(choices_display='WITH_GMT_OFFSET', default='UTC', help_text='The timezone in which the user wants to see times.', use_pytz=False)),
|
||||
('is_device', models.BooleanField(default=False, help_text='Whether the user is a device or a real user.', verbose_name='device')),
|
||||
('is_staff', models.BooleanField(default=False, help_text='Whether the user can log into this admin site.', verbose_name='staff status')),
|
||||
@@ -96,7 +96,7 @@ class Migration(migrations.Migration):
|
||||
migrations.AddField(
|
||||
model_name='resource',
|
||||
name='users',
|
||||
field=models.ManyToManyField(related_name='resources', through='core.ResourceAccess', through_fields=('resource', 'user'), to=settings.AUTH_USER_MODEL),
|
||||
field=models.ManyToManyField(related_name='resources', through='core.ResourceAccess', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='resourceaccess',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Generated by Django 5.0.7 on 2024-08-07 14:39
|
||||
from django.conf import settings
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
@@ -13,6 +13,6 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name='user',
|
||||
name='language',
|
||||
field=models.CharField(choices=settings.LANGUAGES, default=settings.LANGUAGE_CODE, help_text='The language in which the user wants to see the interface.', max_length=10, verbose_name='language'),
|
||||
field=models.CharField(choices="(('en-us', 'English'), ('fr-fr', 'French'))", default='en-us', help_text='The language in which the user wants to see the interface.', max_length=10, verbose_name='language'),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Generated by Django 5.1.8 on 2025-04-22 14:52
|
||||
from django.conf import settings
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
@@ -13,6 +13,6 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name='user',
|
||||
name='language',
|
||||
field=models.CharField(choices=settings.LANGUAGES, default=settings.LANGUAGE_CODE, help_text='The language in which the user wants to see the interface.', max_length=10, verbose_name='language'),
|
||||
field=models.CharField(choices="(('en-us', 'English'), ('fr-fr', 'French'), ('nl-nl', 'Dutch'))", default='en-us', help_text='The language in which the user wants to see the interface.', max_length=10, verbose_name='language'),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -167,7 +167,6 @@ class NotificationService:
|
||||
owner_access.user.timezone
|
||||
).strftime("%H:%M"),
|
||||
"download_link": f"{get_recording_download_base_url()}/{recording.id}",
|
||||
"context_language": owner_access.user.language,
|
||||
}
|
||||
|
||||
headers = {
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
{% load i18n %}
|
||||
{% get_current_language as LANGUAGE %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{ LANGUAGE }}">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>{% trans "Error" %}</title>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>{{ title|default:_("Error") }}</h1>
|
||||
<p>{{ message|default:_("Something went wrong.") }}</p>
|
||||
<button onclick="window.close()">{% trans "Close" %}</button>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
{% load i18n %}
|
||||
{% get_current_language as LANGUAGE %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{ LANGUAGE }}">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>{% trans "Authentication Success" %}</title>
|
||||
</head>
|
||||
<body>
|
||||
<script>
|
||||
window.close();
|
||||
</script>
|
||||
<p>{% trans "Session stored successfully. This window will close automatically." %}</p>
|
||||
<p>{% trans "If it doesn't close" %}, <a href="javascript:window.close()">{% trans "click here" %}</a>.</p>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -8,7 +8,6 @@ import random
|
||||
from unittest import mock
|
||||
from uuid import uuid4
|
||||
|
||||
from django.core.exceptions import SuspiciousOperation
|
||||
from django.urls import reverse
|
||||
|
||||
import pytest
|
||||
@@ -133,7 +132,11 @@ def test_update_participant_success(mock_livekit_client):
|
||||
1,
|
||||
2,
|
||||
], # [TrackSource.CAMERA, TrackSource.MICROPHONE]
|
||||
"hidden": False,
|
||||
"recorder": False,
|
||||
"can_update_metadata": True,
|
||||
"agent": False,
|
||||
"can_subscribe_metrics": False,
|
||||
},
|
||||
"name": "John Doe",
|
||||
}
|
||||
@@ -148,151 +151,6 @@ def test_update_participant_success(mock_livekit_client):
|
||||
mock_livekit_client.aclose.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"permission_payload",
|
||||
[
|
||||
{}, # empty dict is valid
|
||||
{"can_subscribe": True},
|
||||
{"can_publish": True},
|
||||
{"can_publish_data": True},
|
||||
{"can_publish_sources": [1, 2]},
|
||||
{"can_update_metadata": True},
|
||||
],
|
||||
)
|
||||
def test_update_participant_permission_fields_are_optional(
|
||||
mock_livekit_client, permission_payload
|
||||
):
|
||||
"""Test that each required permission field can be passed individually."""
|
||||
client = APIClient()
|
||||
room = RoomFactory()
|
||||
user = UserFactory()
|
||||
UserResourceAccessFactory(
|
||||
resource=room, user=user, role=random.choice(["administrator", "owner"])
|
||||
)
|
||||
client.force_authenticate(user=user)
|
||||
|
||||
payload = {
|
||||
"participant_identity": str(uuid4()),
|
||||
"permission": permission_payload,
|
||||
}
|
||||
|
||||
url = reverse("rooms-update-participant", kwargs={"pk": room.id})
|
||||
response = client.post(url, payload, format="json")
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.data == {"status": "success"}
|
||||
|
||||
mock_livekit_client.room.update_participant.assert_called_once()
|
||||
mock_livekit_client.aclose.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"value,permission_key",
|
||||
[
|
||||
(False, "hidden"),
|
||||
(True, "hidden"),
|
||||
(False, "recorder"),
|
||||
(True, "recorder"),
|
||||
(False, "agent"),
|
||||
(True, "agent"),
|
||||
],
|
||||
)
|
||||
@mock.patch("core.api.serializers.SuspiciousOperation", side_effect=SuspiciousOperation)
|
||||
def test_update_participant_suspicious_permission(
|
||||
mock_suspicious, value, permission_key
|
||||
):
|
||||
"""Test update participant raises 400 when a restricted permission is set."""
|
||||
client = APIClient()
|
||||
room = RoomFactory()
|
||||
user = UserFactory()
|
||||
UserResourceAccessFactory(
|
||||
resource=room, user=user, role=random.choice(["administrator", "owner"])
|
||||
)
|
||||
client.force_authenticate(user=user)
|
||||
|
||||
payload = {
|
||||
"participant_identity": str(uuid4()),
|
||||
"permission": {
|
||||
"can_subscribe": True,
|
||||
"can_publish": True,
|
||||
"can_publish_data": True,
|
||||
"can_update_metadata": False,
|
||||
permission_key: value,
|
||||
},
|
||||
}
|
||||
|
||||
url = reverse("rooms-update-participant", kwargs={"pk": room.id})
|
||||
response = client.post(url, payload, format="json")
|
||||
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
mock_suspicious.assert_called_once_with(
|
||||
f"Setting the following participant permissions is not allowed: {permission_key}."
|
||||
)
|
||||
|
||||
|
||||
@mock.patch("core.api.serializers.SuspiciousOperation", side_effect=SuspiciousOperation)
|
||||
def test_update_participant_suspicious_permission_multiple(mock_suspicious):
|
||||
"""Test update participant raises 400 when multiple suspicious permissions are set."""
|
||||
client = APIClient()
|
||||
room = RoomFactory()
|
||||
user = UserFactory()
|
||||
UserResourceAccessFactory(
|
||||
resource=room, user=user, role=random.choice(["administrator", "owner"])
|
||||
)
|
||||
client.force_authenticate(user=user)
|
||||
|
||||
payload = {
|
||||
"participant_identity": str(uuid4()),
|
||||
"permission": {
|
||||
"can_subscribe": True,
|
||||
"can_publish": True,
|
||||
"can_publish_data": True,
|
||||
"hidden": True,
|
||||
"recorder": False,
|
||||
"can_update_metadata": False,
|
||||
"agent": True,
|
||||
"can_subscribe_metrics": False,
|
||||
},
|
||||
}
|
||||
|
||||
url = reverse("rooms-update-participant", kwargs={"pk": room.id})
|
||||
response = client.post(url, payload, format="json")
|
||||
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
mock_suspicious.assert_called_once_with(
|
||||
"Setting the following participant permissions is not allowed: hidden, recorder, agent."
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("value", (False, True))
|
||||
def test_update_participant_unimplemented_can_subscribe_metrics(value):
|
||||
"""Test update participant raises 400 when can_subscribe_metrics is set."""
|
||||
client = APIClient()
|
||||
room = RoomFactory()
|
||||
user = UserFactory()
|
||||
UserResourceAccessFactory(
|
||||
resource=room, user=user, role=random.choice(["administrator", "owner"])
|
||||
)
|
||||
client.force_authenticate(user=user)
|
||||
|
||||
payload = {
|
||||
"participant_identity": str(uuid4()),
|
||||
"permission": {
|
||||
"can_subscribe": True,
|
||||
"can_publish": True,
|
||||
"can_publish_data": True,
|
||||
"can_update_metadata": False,
|
||||
"can_subscribe_metrics": value,
|
||||
},
|
||||
}
|
||||
|
||||
url = reverse("rooms-update-participant", kwargs={"pk": room.id})
|
||||
response = client.post(url, payload, format="json")
|
||||
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
assert "can_subscribe_metrics" in str(response.data)
|
||||
|
||||
|
||||
def test_update_participant_forbidden_without_access():
|
||||
"""Test update participant returns 403 when user lacks room privileges."""
|
||||
client = APIClient()
|
||||
@@ -368,17 +226,7 @@ def test_update_participant_invalid_permission():
|
||||
response = client.post(url, payload, format="json")
|
||||
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
assert response.json() == {
|
||||
"permission": [
|
||||
{
|
||||
"type": "extra_forbidden",
|
||||
"loc": ["invalid-attributes"],
|
||||
"msg": "Extra inputs are not permitted",
|
||||
"input": "True",
|
||||
"url": "https://errors.pydantic.dev/2.12/v/extra_forbidden",
|
||||
},
|
||||
]
|
||||
}
|
||||
assert "Invalid permission" in str(response.data)
|
||||
|
||||
|
||||
def test_update_participant_wrong_metadata_attributes():
|
||||
|
||||
@@ -199,308 +199,3 @@ def test_start_recording_success(
|
||||
access = recording.accesses.first()
|
||||
assert access.user == user
|
||||
assert access.role == "owner"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("value", ["fr", "en", "nl", "de"])
|
||||
def test_start_recording_options_language_valid(
|
||||
settings, mock_worker_service_factory, mock_worker_manager, value
|
||||
):
|
||||
"""Should accept a valid ISO 639-1 language code."""
|
||||
settings.RECORDING_ENABLE = True
|
||||
room = RoomFactory()
|
||||
user = UserFactory()
|
||||
room.accesses.create(user=user, role="owner")
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
response = client.post(
|
||||
f"/api/v1.0/rooms/{room.id}/start-recording/",
|
||||
{"mode": "screen_recording", "options": {"language": value}},
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
|
||||
recording = Recording.objects.get(room=room)
|
||||
assert recording.options == {"language": value}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("value", ["invalid-value", "francais", "123"])
|
||||
def test_start_recording_options_language_not_validated(
|
||||
settings, mock_worker_service_factory, mock_worker_manager, value
|
||||
):
|
||||
"""Invalid language codes are currently accepted — no format validation yet.
|
||||
|
||||
TODO: tighten this once language validation is introduced.
|
||||
"""
|
||||
settings.RECORDING_ENABLE = True
|
||||
room = RoomFactory()
|
||||
user = UserFactory()
|
||||
room.accesses.create(user=user, role="owner")
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
response = client.post(
|
||||
f"/api/v1.0/rooms/{room.id}/start-recording/",
|
||||
{"mode": "screen_recording", "options": {"language": value}},
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
|
||||
|
||||
def test_start_recording_options_language_null(
|
||||
settings, mock_worker_service_factory, mock_worker_manager
|
||||
):
|
||||
"""Should accept null language (triggers auto-detection)."""
|
||||
settings.RECORDING_ENABLE = True
|
||||
room = RoomFactory()
|
||||
user = UserFactory()
|
||||
room.accesses.create(user=user, role="owner")
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
response = client.post(
|
||||
f"/api/v1.0/rooms/{room.id}/start-recording/",
|
||||
{"mode": "screen_recording", "options": {"language": None}},
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
recording = Recording.objects.get(room=room)
|
||||
assert recording.options == {}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("value", [True, 1, "y", "on", "true", "yes", "t"])
|
||||
def test_start_recording_options_transcribe_valid_true(
|
||||
settings, mock_worker_service_factory, mock_worker_manager, value
|
||||
):
|
||||
"""Should accept transcribe with any valid pydantic true values."""
|
||||
settings.RECORDING_ENABLE = True
|
||||
room = RoomFactory()
|
||||
user = UserFactory()
|
||||
room.accesses.create(user=user, role="owner")
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
response = client.post(
|
||||
f"/api/v1.0/rooms/{room.id}/start-recording/",
|
||||
{"mode": "screen_recording", "options": {"transcribe": value}},
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
recording = Recording.objects.get(room=room)
|
||||
assert recording.options == {"transcribe": True}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("value", [False, 0, "n", "off", "false", "no", "f"])
|
||||
def test_start_recording_options_transcribe_valid_false(
|
||||
settings, mock_worker_service_factory, mock_worker_manager, value
|
||||
):
|
||||
"""Should accept transcribe with any valid pydantic false values."""
|
||||
settings.RECORDING_ENABLE = True
|
||||
room = RoomFactory()
|
||||
user = UserFactory()
|
||||
room.accesses.create(user=user, role="owner")
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
response = client.post(
|
||||
f"/api/v1.0/rooms/{room.id}/start-recording/",
|
||||
{"mode": "screen_recording", "options": {"transcribe": value}},
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
recording = Recording.objects.get(room=room)
|
||||
assert recording.options == {"transcribe": False}
|
||||
|
||||
|
||||
def test_start_recording_options_transcribe_null(
|
||||
settings, mock_worker_service_factory, mock_worker_manager
|
||||
):
|
||||
"""Should accept transcribe=null (falls back to application default)."""
|
||||
settings.RECORDING_ENABLE = True
|
||||
room = RoomFactory()
|
||||
user = UserFactory()
|
||||
room.accesses.create(user=user, role="owner")
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
response = client.post(
|
||||
f"/api/v1.0/rooms/{room.id}/start-recording/",
|
||||
{"mode": "screen_recording", "options": {"transcribe": None}},
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
recording = Recording.objects.get(room=room)
|
||||
assert recording.options == {}
|
||||
|
||||
|
||||
def test_start_recording_options_null(
|
||||
settings, mock_worker_service_factory, mock_worker_manager
|
||||
):
|
||||
"""Should accept options=null."""
|
||||
settings.RECORDING_ENABLE = True
|
||||
room = RoomFactory()
|
||||
user = UserFactory()
|
||||
room.accesses.create(user=user, role="owner")
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
response = client.post(
|
||||
f"/api/v1.0/rooms/{room.id}/start-recording/",
|
||||
{"mode": "screen_recording", "options": None},
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
recording = Recording.objects.get(room=room)
|
||||
assert recording.options == {}
|
||||
|
||||
|
||||
def test_start_recording_options_omitted(
|
||||
settings, mock_worker_service_factory, mock_worker_manager
|
||||
):
|
||||
"""Should accept a request with no options field at all."""
|
||||
settings.RECORDING_ENABLE = True
|
||||
room = RoomFactory()
|
||||
user = UserFactory()
|
||||
room.accesses.create(user=user, role="owner")
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
response = client.post(
|
||||
f"/api/v1.0/rooms/{room.id}/start-recording/",
|
||||
{"mode": "screen_recording"},
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
recording = Recording.objects.get(room=room)
|
||||
assert recording.options == {}
|
||||
|
||||
|
||||
def test_start_recording_options_unknown_field_rejected(settings):
|
||||
"""Should reject unknown fields in options (extra='forbid')."""
|
||||
settings.RECORDING_ENABLE = True
|
||||
room = RoomFactory()
|
||||
user = UserFactory()
|
||||
room.accesses.create(user=user, role="owner")
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
response = client.post(
|
||||
f"/api/v1.0/rooms/{room.id}/start-recording/",
|
||||
{"mode": "screen_recording", "options": {"unknown_field": "value"}},
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
|
||||
|
||||
@pytest.mark.parametrize("value", ["foo", 12])
|
||||
def test_start_recording_options_invalid_transcribe_type(settings, value):
|
||||
"""Should reject non-boolean transcribe values."""
|
||||
settings.RECORDING_ENABLE = True
|
||||
room = RoomFactory()
|
||||
user = UserFactory()
|
||||
room.accesses.create(user=user, role="owner")
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
response = client.post(
|
||||
f"/api/v1.0/rooms/{room.id}/start-recording/",
|
||||
{"mode": "screen_recording", "options": {"transcribe": value}},
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
|
||||
|
||||
@pytest.mark.parametrize("value", ["screen_recording", "transcript"])
|
||||
def test_start_recording_options_original_mode_valid(
|
||||
settings, mock_worker_service_factory, mock_worker_manager, value
|
||||
):
|
||||
"""Should accept valid recording mode choices for original_mode."""
|
||||
settings.RECORDING_ENABLE = True
|
||||
room = RoomFactory()
|
||||
user = UserFactory()
|
||||
room.accesses.create(user=user, role="owner")
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
response = client.post(
|
||||
f"/api/v1.0/rooms/{room.id}/start-recording/",
|
||||
{"mode": "screen_recording", "options": {"original_mode": value}},
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
recording = Recording.objects.get(room=room)
|
||||
assert recording.options == {"original_mode": value}
|
||||
|
||||
|
||||
def test_start_recording_options_original_mode_null(
|
||||
settings, mock_worker_service_factory, mock_worker_manager
|
||||
):
|
||||
"""Should accept original_mode=null."""
|
||||
settings.RECORDING_ENABLE = True
|
||||
room = RoomFactory()
|
||||
user = UserFactory()
|
||||
room.accesses.create(user=user, role="owner")
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
response = client.post(
|
||||
f"/api/v1.0/rooms/{room.id}/start-recording/",
|
||||
{"mode": "screen_recording", "options": {"original_mode": None}},
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
recording = Recording.objects.get(room=room)
|
||||
assert recording.options == {}
|
||||
|
||||
|
||||
def test_start_recording_options_original_mode_omitted(
|
||||
settings, mock_worker_service_factory, mock_worker_manager
|
||||
):
|
||||
"""Should accept a request with original_mode omitted."""
|
||||
settings.RECORDING_ENABLE = True
|
||||
room = RoomFactory()
|
||||
user = UserFactory()
|
||||
room.accesses.create(user=user, role="owner")
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
response = client.post(
|
||||
f"/api/v1.0/rooms/{room.id}/start-recording/",
|
||||
{"mode": "screen_recording", "options": {}},
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
recording = Recording.objects.get(room=room)
|
||||
assert recording.options == {}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("value", ["invalid_mode", "foo", 123, "SCREEN_RECORDING"])
|
||||
def test_start_recording_options_original_mode_invalid(settings, value):
|
||||
"""Should reject invalid recording mode values for original_mode."""
|
||||
settings.RECORDING_ENABLE = True
|
||||
room = RoomFactory()
|
||||
user = UserFactory()
|
||||
room.accesses.create(user=user, role="owner")
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
response = client.post(
|
||||
f"/api/v1.0/rooms/{room.id}/start-recording/",
|
||||
{"mode": "screen_recording", "options": {"original_mode": value}},
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
|
||||
@@ -6,6 +6,8 @@ from django.urls import include, path
|
||||
from lasuite.oidc_login.urls import urlpatterns as oidc_urls
|
||||
from rest_framework.routers import DefaultRouter
|
||||
|
||||
from core.addons import views as addons_views
|
||||
from core.addons import viewsets as addons_viewsets
|
||||
from core.api import get_frontend_configuration, viewsets
|
||||
from core.external_api import viewsets as external_viewsets
|
||||
|
||||
@@ -26,12 +28,24 @@ external_router.register(
|
||||
basename="external_application",
|
||||
)
|
||||
|
||||
# - Addons API
|
||||
addons_router = DefaultRouter()
|
||||
addons_router.register(
|
||||
"addons/sessions",
|
||||
addons_viewsets.AuthSessionViewSet,
|
||||
basename="addons_auth_sessions",
|
||||
)
|
||||
|
||||
external_router.register(
|
||||
"rooms",
|
||||
external_viewsets.RoomViewSet,
|
||||
basename="external_room",
|
||||
)
|
||||
|
||||
|
||||
addons_urls = addons_router.urls if settings.ADDONS_ENABLED else []
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path(
|
||||
f"api/{settings.API_VERSION}/",
|
||||
@@ -39,12 +53,26 @@ urlpatterns = [
|
||||
[
|
||||
*router.urls,
|
||||
*oidc_urls,
|
||||
*addons_urls,
|
||||
path("config/", get_frontend_configuration, name="config"),
|
||||
]
|
||||
),
|
||||
),
|
||||
]
|
||||
|
||||
if settings.ADDONS_ENABLED:
|
||||
urlpatterns.append(
|
||||
path(
|
||||
"addons/",
|
||||
include(
|
||||
[
|
||||
path("transit/", addons_views.transit_page, name="transit_page"),
|
||||
path("redirect/", addons_views.redirect_page, name="redirect_page"),
|
||||
]
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
if settings.EXTERNAL_API_ENABLED:
|
||||
urlpatterns.append(
|
||||
path(
|
||||
|
||||
Binary file not shown.
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-12-29 15:15+0000\n"
|
||||
"POT-Creation-Date: 2026-01-26 15:40+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@@ -17,6 +17,30 @@ msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#: core/addons/views.py:24
|
||||
msgid "Session ID is required."
|
||||
msgstr "Sitzungs-ID ist erforderlich."
|
||||
|
||||
#: core/addons/views.py:29
|
||||
msgid "Session not found or expired."
|
||||
msgstr "Sitzung nicht gefunden oder abgelaufen."
|
||||
|
||||
#: core/addons/views.py:32
|
||||
msgid "Invalid session state."
|
||||
msgstr "Ungültiger Sitzungsstatus."
|
||||
|
||||
#: core/addons/views.py:45
|
||||
msgid "Authentication required."
|
||||
msgstr "Authentifizierung erforderlich."
|
||||
|
||||
#: core/addons/views.py:50
|
||||
msgid "No active session found."
|
||||
msgstr "Keine aktive Sitzung gefunden."
|
||||
|
||||
#: core/addons/views.py:55
|
||||
msgid "Invalid or expired session."
|
||||
msgstr "Ungültige oder abgelaufene Sitzung."
|
||||
|
||||
#: core/admin.py:29
|
||||
msgid "Personal info"
|
||||
msgstr "Persönliche Informationen"
|
||||
@@ -408,7 +432,7 @@ msgstr "Anwendungsdomain"
|
||||
msgid "Application domains"
|
||||
msgstr "Anwendungsdomains"
|
||||
|
||||
#: core/recording/event/notification.py:94
|
||||
#: core/recording/event/notification.py:116
|
||||
msgid "Your recording is ready"
|
||||
msgstr "Ihre Aufzeichnung ist bereit"
|
||||
|
||||
@@ -417,6 +441,30 @@ msgstr "Ihre Aufzeichnung ist bereit"
|
||||
msgid "Video call in progress: {sender.email} is waiting for you to connect"
|
||||
msgstr "Videoanruf läuft: {sender.email} wartet auf Ihre Teilnahme"
|
||||
|
||||
#: core/templates/addons/error.html:7 core/templates/addons/error.html:11
|
||||
msgid "Error"
|
||||
msgstr "Fehler"
|
||||
|
||||
#: core/templates/addons/error.html:12
|
||||
msgid "Something went wrong."
|
||||
msgstr "Etwas ist schiefgelaufen."
|
||||
|
||||
#: core/templates/addons/error.html:13
|
||||
msgid "Close"
|
||||
msgstr "Schließen"
|
||||
|
||||
#: core/templates/addons/redirect_success.html:7
|
||||
msgid "Authentication Success"
|
||||
msgstr "Authentifizierung erfolgreich"
|
||||
|
||||
#: core/templates/addons/redirect_success.html:13
|
||||
msgid "Session stored successfully. This window will close automatically."
|
||||
msgstr "Sitzung erfolgreich gespeichert. Dieses Fenster wird automatisch geschlossen."
|
||||
|
||||
#: core/templates/addons/redirect_success.html:14
|
||||
msgid "If it doesn't close"
|
||||
msgstr "Falls es sich nicht schließt"
|
||||
|
||||
#: core/templates/mail/html/invitation.html:159
|
||||
#: core/templates/mail/html/screen_recording.html:159
|
||||
#: core/templates/mail/text/invitation.txt:3
|
||||
|
||||
Binary file not shown.
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-12-29 15:15+0000\n"
|
||||
"POT-Creation-Date: 2026-01-26 15:40+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@@ -17,6 +17,30 @@ msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#: core/addons/views.py:24
|
||||
msgid "Session ID is required."
|
||||
msgstr "Session ID is required."
|
||||
|
||||
#: core/addons/views.py:29
|
||||
msgid "Session not found or expired."
|
||||
msgstr "Session not found or expired."
|
||||
|
||||
#: core/addons/views.py:32
|
||||
msgid "Invalid session state."
|
||||
msgstr "Invalid session state."
|
||||
|
||||
#: core/addons/views.py:45
|
||||
msgid "Authentication required."
|
||||
msgstr "Authentication required."
|
||||
|
||||
#: core/addons/views.py:50
|
||||
msgid "No active session found."
|
||||
msgstr "No active session found."
|
||||
|
||||
#: core/addons/views.py:55
|
||||
msgid "Invalid or expired session."
|
||||
msgstr "Invalid or expired session."
|
||||
|
||||
#: core/admin.py:29
|
||||
msgid "Personal info"
|
||||
msgstr "Personal info"
|
||||
@@ -405,7 +429,7 @@ msgstr "Application domain"
|
||||
msgid "Application domains"
|
||||
msgstr "Application domains"
|
||||
|
||||
#: core/recording/event/notification.py:94
|
||||
#: core/recording/event/notification.py:116
|
||||
msgid "Your recording is ready"
|
||||
msgstr "Your recording is ready"
|
||||
|
||||
@@ -414,6 +438,30 @@ msgstr "Your recording is ready"
|
||||
msgid "Video call in progress: {sender.email} is waiting for you to connect"
|
||||
msgstr "Video call in progress: {sender.email} is waiting for you to connect"
|
||||
|
||||
#: core/templates/addons/error.html:7 core/templates/addons/error.html:11
|
||||
msgid "Error"
|
||||
msgstr "Error"
|
||||
|
||||
#: core/templates/addons/error.html:12
|
||||
msgid "Something went wrong."
|
||||
msgstr "Something went wrong."
|
||||
|
||||
#: core/templates/addons/error.html:13
|
||||
msgid "Close"
|
||||
msgstr "Close"
|
||||
|
||||
#: core/templates/addons/redirect_success.html:7
|
||||
msgid "Authentication Success"
|
||||
msgstr "Authentication Success"
|
||||
|
||||
#: core/templates/addons/redirect_success.html:13
|
||||
msgid "Session stored successfully. This window will close automatically."
|
||||
msgstr "Session stored successfully. This window will close automatically."
|
||||
|
||||
#: core/templates/addons/redirect_success.html:14
|
||||
msgid "If it doesn't close"
|
||||
msgstr "If it doesn't close"
|
||||
|
||||
#: core/templates/mail/html/invitation.html:159
|
||||
#: core/templates/mail/html/screen_recording.html:159
|
||||
#: core/templates/mail/text/invitation.txt:3
|
||||
|
||||
Binary file not shown.
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-12-29 15:15+0000\n"
|
||||
"POT-Creation-Date: 2026-01-26 15:40+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: antoine.lebaud@mail.numerique.gouv.fr\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@@ -17,6 +17,30 @@ msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#: core/addons/views.py:24
|
||||
msgid "Session ID is required."
|
||||
msgstr "L'identifiant de session est requis."
|
||||
|
||||
#: core/addons/views.py:29
|
||||
msgid "Session not found or expired."
|
||||
msgstr "Session introuvable ou expirée."
|
||||
|
||||
#: core/addons/views.py:32
|
||||
msgid "Invalid session state."
|
||||
msgstr "État de session invalide."
|
||||
|
||||
#: core/addons/views.py:45
|
||||
msgid "Authentication required."
|
||||
msgstr "Authentification requise."
|
||||
|
||||
#: core/addons/views.py:50
|
||||
msgid "No active session found."
|
||||
msgstr "Aucune session active trouvée."
|
||||
|
||||
#: core/addons/views.py:55
|
||||
msgid "Invalid or expired session."
|
||||
msgstr "Session invalide ou expirée."
|
||||
|
||||
#: core/admin.py:29
|
||||
msgid "Personal info"
|
||||
msgstr "Informations personnelles"
|
||||
@@ -409,7 +433,7 @@ msgstr "Domaine d’application"
|
||||
msgid "Application domains"
|
||||
msgstr "Domaines d’application"
|
||||
|
||||
#: core/recording/event/notification.py:94
|
||||
#: core/recording/event/notification.py:116
|
||||
msgid "Your recording is ready"
|
||||
msgstr "Votre enregistrement est prêt"
|
||||
|
||||
@@ -418,6 +442,30 @@ msgstr "Votre enregistrement est prêt"
|
||||
msgid "Video call in progress: {sender.email} is waiting for you to connect"
|
||||
msgstr "Appel vidéo en cours : {sender.email} attend que vous vous connectiez"
|
||||
|
||||
#: core/templates/addons/error.html:7 core/templates/addons/error.html:11
|
||||
msgid "Error"
|
||||
msgstr "Erreur"
|
||||
|
||||
#: core/templates/addons/error.html:12
|
||||
msgid "Something went wrong."
|
||||
msgstr "Une erreur s'est produite."
|
||||
|
||||
#: core/templates/addons/error.html:13
|
||||
msgid "Close"
|
||||
msgstr "Fermer"
|
||||
|
||||
#: core/templates/addons/redirect_success.html:7
|
||||
msgid "Authentication Success"
|
||||
msgstr "Authentification réussie"
|
||||
|
||||
#: core/templates/addons/redirect_success.html:13
|
||||
msgid "Session stored successfully. This window will close automatically."
|
||||
msgstr "Session enregistrée avec succès. Cette fenêtre se fermera automatiquement."
|
||||
|
||||
#: core/templates/addons/redirect_success.html:14
|
||||
msgid "If it doesn't close"
|
||||
msgstr "Si elle ne se ferme pas"
|
||||
|
||||
#: core/templates/mail/html/invitation.html:159
|
||||
#: core/templates/mail/html/screen_recording.html:159
|
||||
#: core/templates/mail/text/invitation.txt:3
|
||||
|
||||
Binary file not shown.
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-12-29 15:15+0000\n"
|
||||
"POT-Creation-Date: 2026-01-26 15:40+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@@ -17,6 +17,30 @@ msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#: core/addons/views.py:24
|
||||
msgid "Session ID is required."
|
||||
msgstr "Sessie-ID is vereist."
|
||||
|
||||
#: core/addons/views.py:29
|
||||
msgid "Session not found or expired."
|
||||
msgstr "Sessie niet gevonden of verlopen."
|
||||
|
||||
#: core/addons/views.py:32
|
||||
msgid "Invalid session state."
|
||||
msgstr "Ongeldige sessiestatus."
|
||||
|
||||
#: core/addons/views.py:45
|
||||
msgid "Authentication required."
|
||||
msgstr "Authenticatie vereist."
|
||||
|
||||
#: core/addons/views.py:50
|
||||
msgid "No active session found."
|
||||
msgstr "Geen actieve sessie gevonden."
|
||||
|
||||
#: core/addons/views.py:55
|
||||
msgid "Invalid or expired session."
|
||||
msgstr "Ongeldige of verlopen sessie."
|
||||
|
||||
#: core/admin.py:29
|
||||
msgid "Personal info"
|
||||
msgstr "Persoonlijke informatie"
|
||||
@@ -404,7 +428,7 @@ msgstr "Applicatiedomein"
|
||||
msgid "Application domains"
|
||||
msgstr "Applicatiedomeinen"
|
||||
|
||||
#: core/recording/event/notification.py:94
|
||||
#: core/recording/event/notification.py:116
|
||||
msgid "Your recording is ready"
|
||||
msgstr "Je opname is klaar"
|
||||
|
||||
@@ -413,6 +437,30 @@ msgstr "Je opname is klaar"
|
||||
msgid "Video call in progress: {sender.email} is waiting for you to connect"
|
||||
msgstr "Video-oproep bezig: {sender.email} wacht op je verbinding"
|
||||
|
||||
#: core/templates/addons/error.html:7 core/templates/addons/error.html:11
|
||||
msgid "Error"
|
||||
msgstr "Fout"
|
||||
|
||||
#: core/templates/addons/error.html:12
|
||||
msgid "Something went wrong."
|
||||
msgstr "Er is iets misgegaan."
|
||||
|
||||
#: core/templates/addons/error.html:13
|
||||
msgid "Close"
|
||||
msgstr "Sluiten"
|
||||
|
||||
#: core/templates/addons/redirect_success.html:7
|
||||
msgid "Authentication Success"
|
||||
msgstr "Authenticatie geslaagd"
|
||||
|
||||
#: core/templates/addons/redirect_success.html:13
|
||||
msgid "Session stored successfully. This window will close automatically."
|
||||
msgstr "Sessie succesvol opgeslagen. Dit venster wordt automatisch gesloten."
|
||||
|
||||
#: core/templates/addons/redirect_success.html:14
|
||||
msgid "If it doesn't close"
|
||||
msgstr "Als het niet sluit"
|
||||
|
||||
#: core/templates/mail/html/invitation.html:159
|
||||
#: core/templates/mail/html/screen_recording.html:159
|
||||
#: core/templates/mail/text/invitation.txt:3
|
||||
|
||||
@@ -596,12 +596,6 @@ class Base(Configuration):
|
||||
ALLOW_UNREGISTERED_ROOMS = values.BooleanValue(
|
||||
True, environ_name="ALLOW_UNREGISTERED_ROOMS", environ_prefix=None
|
||||
)
|
||||
# if provided, treat as suspicious (possible privilege escalation attempt).
|
||||
PARTICIPANT_FORBIDDEN_PERMISSION_FIELDS = values.ListValue(
|
||||
["hidden", "recorder", "agent"],
|
||||
environ_name="PARTICIPANT_FORBIDDEN_PERMISSION_FIELDS",
|
||||
environ_prefix=None,
|
||||
)
|
||||
|
||||
# Recording settings
|
||||
RECORDING_ENABLE = values.BooleanValue(
|
||||
@@ -803,6 +797,66 @@ class Base(Configuration):
|
||||
environ_prefix=None,
|
||||
)
|
||||
|
||||
# Addons
|
||||
ADDONS_ENABLED = values.BooleanValue(
|
||||
False,
|
||||
environ_name="ADDONS_ENABLED",
|
||||
environ_prefix=None,
|
||||
)
|
||||
ADDONS_SESSION_ID_LENGTH = values.PositiveIntegerValue(
|
||||
32,
|
||||
environ_name="ADDONS_SESSION_ID_LENGTH",
|
||||
environ_prefix=None,
|
||||
)
|
||||
# Used in cache key generation
|
||||
ADDONS_SESSION_KEY_PREFIX = values.Value(
|
||||
"addons_session_id",
|
||||
environ_name="ADDONS_SESSION_KEY_PREFIX",
|
||||
environ_prefix=None,
|
||||
)
|
||||
# Used as the Django session key in transit page
|
||||
ADDONS_SESSION_KEY_AUTH = values.Value(
|
||||
"addons_session_id",
|
||||
environ_name="ADDONS_SESSION_KEY_AUTH",
|
||||
environ_prefix=None,
|
||||
)
|
||||
ADDONS_SESSION_TIMEOUT = values.PositiveIntegerValue(
|
||||
600, environ_name="ADDONS_SESSION_TIMEOUT", environ_prefix=None
|
||||
)
|
||||
ADDONS_JWT_SECRET_KEY = SecretFileValue(
|
||||
None, environ_name="ADDONS_JWT_SECRET_KEY", environ_prefix=None
|
||||
)
|
||||
ADDONS_JWT_ALG = values.Value(
|
||||
"HS256",
|
||||
environ_name="ADDONS_JWT_ALG",
|
||||
environ_prefix=None,
|
||||
)
|
||||
ADDONS_SCOPES = values.Value(
|
||||
"rooms:create rooms:list",
|
||||
environ_name="ADDONS_SCOPES",
|
||||
environ_prefix=None,
|
||||
)
|
||||
ADDONS_JWT_ISSUER = values.Value(
|
||||
"lasuite-meet",
|
||||
environ_name="ADDONS_JWT_ISSUER",
|
||||
environ_prefix=None,
|
||||
)
|
||||
ADDONS_JWT_AUDIENCE = values.Value(
|
||||
None,
|
||||
environ_name="ADDONS_JWT_AUDIENCE",
|
||||
environ_prefix=None,
|
||||
)
|
||||
ADDONS_JWT_EXPIRATION_SECONDS = values.PositiveIntegerValue(
|
||||
3600,
|
||||
environ_name="ADDONS_JWT_EXPIRATION_SECONDS",
|
||||
environ_prefix=None,
|
||||
)
|
||||
ADDONS_JWT_TOKEN_TYPE = values.Value(
|
||||
"Bearer",
|
||||
environ_name="ADDONS_JWT_TOKEN_TYPE",
|
||||
environ_prefix=None,
|
||||
)
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
@property
|
||||
def ENVIRONMENT(self):
|
||||
|
||||
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "meet"
|
||||
version = "1.9.0"
|
||||
version = "1.8.0"
|
||||
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
@@ -17,13 +17,13 @@ classifiers = [
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Natural Language :: English",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.13",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
]
|
||||
description = "A simple video and phone conferencing tool, powered by LiveKit"
|
||||
keywords = ["Django", "Contacts", "Templates", "RBAC"]
|
||||
license = { file = "LICENSE" }
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.13"
|
||||
requires-python = ">=3.10"
|
||||
dependencies = [
|
||||
"boto3==1.42.49",
|
||||
"Brotli==1.2.0",
|
||||
@@ -39,7 +39,6 @@ dependencies = [
|
||||
"django-redis==6.0.0",
|
||||
"django-storages[s3]==1.14.6",
|
||||
"django-timezone-field>=5.1",
|
||||
"django-pydantic-field==0.5.4",
|
||||
"django==5.2.11",
|
||||
"djangorestframework==3.16.1",
|
||||
"drf_spectacular==0.29.0",
|
||||
@@ -51,7 +50,6 @@ dependencies = [
|
||||
"markdown==3.10.2",
|
||||
"nested-multipart-parser==1.6.0",
|
||||
"psycopg[binary]==3.3.2",
|
||||
"pydantic==2.12.4",
|
||||
"PyJWT==2.11.0",
|
||||
"python-frontmatter==1.1.0",
|
||||
"requests==2.32.5",
|
||||
|
||||
@@ -11,6 +11,12 @@ server {
|
||||
add_header Content-Disposition "attachment; filename=windows-app-web-link";
|
||||
}
|
||||
|
||||
location ~ ^/outlook-addin(/.*)?$ {
|
||||
alias /usr/share/nginx/html/outlook-addin$1;
|
||||
add_header Access-Control-Allow-Origin "*";
|
||||
add_header Cache-Control "no-cache, no-store, must-revalidate";
|
||||
}
|
||||
|
||||
# Serve static files with caching
|
||||
location ~* ^/assets/.*\.(css|js|json|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
||||
expires 30d;
|
||||
|
||||
Generated
+839
-856
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "meet",
|
||||
"private": true,
|
||||
"version": "1.9.0",
|
||||
"version": "1.8.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "panda codegen && vite",
|
||||
@@ -36,7 +36,7 @@
|
||||
"livekit-client": "2.17.1",
|
||||
"posthog-js": "1.342.1",
|
||||
"react": "18.3.1",
|
||||
"react-aria-components": "1.14.0",
|
||||
"react-aria-components": "1.10.1",
|
||||
"react-dom": "18.3.1",
|
||||
"react-i18next": "15.1.1",
|
||||
"use-sound": "5.0.0",
|
||||
|
||||
@@ -4,7 +4,6 @@ import { Button } from '@/primitives'
|
||||
import { RiArrowLeftSLine, RiArrowRightSLine } from '@remixicon/react'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useScreenReaderAnnounce } from '@/hooks/useScreenReaderAnnounce'
|
||||
|
||||
const Heading = styled('h2', {
|
||||
base: {
|
||||
@@ -145,21 +144,6 @@ type Slide = {
|
||||
isAvailableInBeta?: boolean
|
||||
}
|
||||
|
||||
const carouselNavButton = css({
|
||||
_focusVisible: {
|
||||
outline: '2px solid var(--colors-focus-ring) !important',
|
||||
outlineOffset: '1px',
|
||||
},
|
||||
_disabled: {
|
||||
color: 'greyscale.400',
|
||||
cursor: 'default',
|
||||
pointerEvents: 'none',
|
||||
_pressed: {
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// todo - optimize how images are imported
|
||||
const SLIDES: Slide[] = [
|
||||
{
|
||||
@@ -179,45 +163,11 @@ const SLIDES: Slide[] = [
|
||||
export const IntroSlider = () => {
|
||||
const [slideIndex, setSlideIndex] = useState(0)
|
||||
const { t } = useTranslation('home', { keyPrefix: 'introSlider' })
|
||||
const announce = useScreenReaderAnnounce()
|
||||
|
||||
const NUMBER_SLIDES = SLIDES.length
|
||||
|
||||
const goPrev = () => {
|
||||
if (slideIndex === 0) return
|
||||
const newIndex = slideIndex - 1
|
||||
setSlideIndex(newIndex)
|
||||
announce(
|
||||
t('slidePosition', { current: newIndex + 1, total: NUMBER_SLIDES }),
|
||||
'polite',
|
||||
'global'
|
||||
)
|
||||
}
|
||||
|
||||
const goNext = () => {
|
||||
if (slideIndex === NUMBER_SLIDES - 1) return
|
||||
const newIndex = slideIndex + 1
|
||||
setSlideIndex(newIndex)
|
||||
announce(
|
||||
t('slidePosition', { current: newIndex + 1, total: NUMBER_SLIDES }),
|
||||
'polite',
|
||||
'global'
|
||||
)
|
||||
}
|
||||
|
||||
const ariaLabelParams = {
|
||||
current: slideIndex + 1,
|
||||
total: NUMBER_SLIDES,
|
||||
}
|
||||
const previousAriaLabel = t('previous.labelWithPosition', ariaLabelParams)
|
||||
const nextAriaLabel = t('next.labelWithPosition', ariaLabelParams)
|
||||
|
||||
return (
|
||||
<Container
|
||||
role="region"
|
||||
aria-roledescription="carousel"
|
||||
aria-label={t('carouselLabel')}
|
||||
>
|
||||
<Container>
|
||||
<div
|
||||
className={css({
|
||||
display: 'flex',
|
||||
@@ -230,10 +180,10 @@ export const IntroSlider = () => {
|
||||
<Button
|
||||
variant="secondaryText"
|
||||
square
|
||||
className={carouselNavButton}
|
||||
aria-label={previousAriaLabel}
|
||||
aria-disabled={slideIndex === 0}
|
||||
onPress={goPrev}
|
||||
aria-label={t('previous.label')}
|
||||
tooltip={t('previous.tooltip')}
|
||||
onPress={() => setSlideIndex(slideIndex - 1)}
|
||||
isDisabled={slideIndex == 0}
|
||||
>
|
||||
<RiArrowLeftSLine />
|
||||
</Button>
|
||||
@@ -241,11 +191,7 @@ export const IntroSlider = () => {
|
||||
</ButtonContainer>
|
||||
<SlideContainer>
|
||||
{SLIDES.map((slide, index) => (
|
||||
<Slide
|
||||
aria-hidden={index !== slideIndex}
|
||||
visible={index === slideIndex}
|
||||
key={index}
|
||||
>
|
||||
<Slide visible={index == slideIndex} key={index}>
|
||||
<Image src={slide.src} alt="" role="presentation" />
|
||||
<TextAnimation visible={index == slideIndex}>
|
||||
<Heading>{t(`${slide.key}.title`)}</Heading>
|
||||
@@ -259,10 +205,10 @@ export const IntroSlider = () => {
|
||||
<Button
|
||||
variant="secondaryText"
|
||||
square
|
||||
className={carouselNavButton}
|
||||
aria-label={nextAriaLabel}
|
||||
aria-disabled={slideIndex === NUMBER_SLIDES - 1}
|
||||
onPress={goNext}
|
||||
aria-label={t('next.label')}
|
||||
tooltip={t('next.tooltip')}
|
||||
onPress={() => setSlideIndex(slideIndex + 1)}
|
||||
isDisabled={slideIndex == NUMBER_SLIDES - 1}
|
||||
>
|
||||
<RiArrowRightSLine />
|
||||
</Button>
|
||||
|
||||
@@ -5,42 +5,37 @@ import { isRoomValid } from '@/features/rooms'
|
||||
|
||||
export const JoinMeetingDialog = () => {
|
||||
const { t } = useTranslation('home')
|
||||
|
||||
const handleSubmit = (data: { roomId?: FormDataEntryValue }) => {
|
||||
const roomId = (data.roomId as string)
|
||||
.trim()
|
||||
.replace(`${window.location.origin}/`, '')
|
||||
navigateTo('room', roomId)
|
||||
}
|
||||
|
||||
const validateRoomId = (value: string) => {
|
||||
const trimmed = value.trim()
|
||||
if (!trimmed) return null
|
||||
return !isRoomValid(trimmed) ? (
|
||||
<>
|
||||
<p>{t('joinInputError')}</p>
|
||||
<Ul>
|
||||
<li>{window.location.origin}/uio-azer-jkl</li>
|
||||
<li>uio-azer-jkl</li>
|
||||
</Ul>
|
||||
</>
|
||||
) : null
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog title={t('joinMeeting')}>
|
||||
<Form onSubmit={handleSubmit} submitLabel={t('joinInputSubmit')}>
|
||||
{/* eslint-disable jsx-a11y/no-autofocus -- Focus on input when modal opens, required for accessibility */}
|
||||
<Form
|
||||
onSubmit={(data) => {
|
||||
navigateTo(
|
||||
'room',
|
||||
(data.roomId as string)
|
||||
.trim()
|
||||
.replace(`${window.location.origin}/`, '')
|
||||
)
|
||||
}}
|
||||
submitLabel={t('joinInputSubmit')}
|
||||
>
|
||||
<Field
|
||||
type="text"
|
||||
autoFocus
|
||||
isRequired
|
||||
name="roomId"
|
||||
label={t('joinInputLabel')}
|
||||
description={t('joinInputExample', {
|
||||
example: window.origin + '/azer-tyu-qsdf',
|
||||
})}
|
||||
validate={validateRoomId}
|
||||
validate={(value) => {
|
||||
return !isRoomValid(value.trim()) ? (
|
||||
<>
|
||||
<p>{t('joinInputError')}</p>
|
||||
<Ul>
|
||||
<li>{window.location.origin}/uio-azer-jkl</li>
|
||||
<li>uio-azer-jkl</li>
|
||||
</Ul>
|
||||
</>
|
||||
) : null
|
||||
}}
|
||||
/>
|
||||
</Form>
|
||||
<H lvl={2}>{t('joinMeetingTipHeading')}</H>
|
||||
|
||||
@@ -65,7 +65,7 @@ export const InviteDialog = (props: Omit<DialogProps, 'title'>) => {
|
||||
alignItems="left"
|
||||
justify="start"
|
||||
gap={0}
|
||||
style={{ maxWidth: '100%', overflow: 'visible' }}
|
||||
style={{ maxWidth: '100%', overflow: 'hidden' }}
|
||||
>
|
||||
<Heading slot="title" level={2} className={text({ variant: 'h2' })}>
|
||||
{t('heading')}
|
||||
@@ -93,7 +93,7 @@ export const InviteDialog = (props: Omit<DialogProps, 'title'>) => {
|
||||
flexDirection: 'column',
|
||||
marginTop: '0.5rem',
|
||||
gap: '1rem',
|
||||
overflow: 'visible',
|
||||
overflow: 'hidden',
|
||||
})}
|
||||
>
|
||||
<div
|
||||
|
||||
@@ -753,7 +753,7 @@ export const Join = ({
|
||||
try {
|
||||
saveVideoInputDeviceId(id)
|
||||
if (videoTrack) {
|
||||
await videoTrack.setDeviceId({ exact: id })
|
||||
await await videoTrack.setDeviceId({ exact: id })
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to switch camera device', err)
|
||||
|
||||
@@ -4,7 +4,7 @@ import { cva } from '@/styled-system/css'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { styled, VStack } from '@/styled-system/jsx'
|
||||
import { usePostHog } from 'posthog-js/react'
|
||||
import type { PostHog } from 'posthog-js'
|
||||
import { PostHog } from 'posthog-js'
|
||||
import { Button as RACButton } from 'react-aria-components'
|
||||
import { useIsAnalyticsEnabled } from '@/features/analytics/hooks/useIsAnalyticsEnabled'
|
||||
|
||||
|
||||
@@ -74,17 +74,13 @@ export const useWaitingParticipants = () => {
|
||||
): Promise<void> => {
|
||||
try {
|
||||
setListEnabled(false)
|
||||
|
||||
await Promise.all(
|
||||
waitingParticipants.map((participant) =>
|
||||
enterRoom({
|
||||
roomId: roomId,
|
||||
allowEntry,
|
||||
participantId: participant.id,
|
||||
})
|
||||
)
|
||||
)
|
||||
|
||||
for (const participant of waitingParticipants) {
|
||||
await enterRoom({
|
||||
roomId: roomId,
|
||||
allowEntry,
|
||||
participantId: participant.id,
|
||||
})
|
||||
}
|
||||
await refetchWaiting()
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
|
||||
@@ -13,7 +13,7 @@ import { useSettingsDialog } from '@/features/settings/hook/useSettingsDialog'
|
||||
import { SettingsDialogExtendedKey } from '@/features/settings/type'
|
||||
|
||||
const IDLE_DISCONNECT_TIMEOUT_MS = 120000 // 2 minutes
|
||||
const COUNTDOWN_ANNOUNCEMENT_SECONDS = new Set([90, 60, 30])
|
||||
const COUNTDOWN_ANNOUNCEMENT_SECONDS = [90, 60, 30]
|
||||
const FINAL_COUNTDOWN_SECONDS = 10
|
||||
|
||||
export const IsIdleDisconnectModal = () => {
|
||||
@@ -58,7 +58,7 @@ export const IsIdleDisconnectModal = () => {
|
||||
if (!connectionObserverSnap.isIdleDisconnectModalOpen) return
|
||||
|
||||
const shouldAnnounce =
|
||||
COUNTDOWN_ANNOUNCEMENT_SECONDS.has(remainingSeconds) ||
|
||||
COUNTDOWN_ANNOUNCEMENT_SECONDS.includes(remainingSeconds) ||
|
||||
remainingSeconds <= FINAL_COUNTDOWN_SECONDS
|
||||
|
||||
if (shouldAnnounce && remainingSeconds !== lastAnnouncementRef.current) {
|
||||
|
||||
@@ -1,21 +1,8 @@
|
||||
import type { CSSProperties } from 'react'
|
||||
import { Text } from '@/primitives'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useParticipantInfo } from '@livekit/components-react'
|
||||
import { Participant } from 'livekit-client'
|
||||
|
||||
const participantNameStyles: CSSProperties = {
|
||||
paddingBottom: '0.1rem',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
}
|
||||
|
||||
const participantNameScreenShareStyles: CSSProperties = {
|
||||
...participantNameStyles,
|
||||
marginLeft: '0.4rem',
|
||||
}
|
||||
|
||||
export const ParticipantName = ({
|
||||
participant,
|
||||
isScreenShare = false,
|
||||
@@ -30,14 +17,26 @@ export const ParticipantName = ({
|
||||
|
||||
if (isScreenShare) {
|
||||
return (
|
||||
<Text variant="sm" style={participantNameScreenShareStyles}>
|
||||
<Text
|
||||
variant="sm"
|
||||
style={{
|
||||
paddingBottom: '0.1rem',
|
||||
marginLeft: '0.4rem',
|
||||
}}
|
||||
>
|
||||
{t('screenShare', { name: displayedName })}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Text variant="sm" style={participantNameStyles} aria-hidden="true">
|
||||
<Text
|
||||
variant="sm"
|
||||
style={{
|
||||
paddingBottom: '0.1rem',
|
||||
}}
|
||||
aria-hidden="true"
|
||||
>
|
||||
{displayedName}
|
||||
</Text>
|
||||
)
|
||||
|
||||
@@ -183,7 +183,7 @@ export const ParticipantTile: (
|
||||
}}
|
||||
>
|
||||
{isHandRaised && !isScreenShare && (
|
||||
<span>
|
||||
<>
|
||||
<span>{positionInQueue}</span>
|
||||
<RiHand
|
||||
color="black"
|
||||
@@ -197,7 +197,7 @@ export const ParticipantTile: (
|
||||
animationIterationCount: '2',
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
{isScreenShare && (
|
||||
<ScreenShareIcon
|
||||
@@ -210,12 +210,10 @@ export const ParticipantTile: (
|
||||
{isEncrypted && !isScreenShare && (
|
||||
<LockLockedIcon style={{ marginRight: '0.25rem' }} />
|
||||
)}
|
||||
<div className="lk-participant-name-wrapper">
|
||||
<ParticipantName
|
||||
isScreenShare={isScreenShare}
|
||||
participant={trackReference.participant}
|
||||
/>
|
||||
</div>
|
||||
<ParticipantName
|
||||
isScreenShare={isScreenShare}
|
||||
participant={trackReference.participant}
|
||||
/>
|
||||
</div>
|
||||
</HStack>
|
||||
<ConnectionQualityIndicator className="lk-participant-metadata-item" />
|
||||
@@ -231,9 +229,9 @@ export const ParticipantTile: (
|
||||
)}
|
||||
</ParticipantContextIfNeeded>
|
||||
</TrackRefContextIfNeeded>
|
||||
<div className="shortcut-hint-wrapper">
|
||||
{hasKeyboardFocus && (
|
||||
<KeyboardShortcutHint>{t('toolbarHint')}</KeyboardShortcutHint>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
+1
-16
@@ -1,6 +1,5 @@
|
||||
import { ToggleButton } from '@/primitives'
|
||||
import { useRegisterKeyboardShortcut } from '@/features/shortcuts/useRegisterKeyboardShortcut'
|
||||
import { useScreenReaderAnnounce } from '@/hooks/useScreenReaderAnnounce'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { appendShortcutLabel } from '@/features/shortcuts/utils'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@@ -88,24 +87,10 @@ export const ToggleDevice = <T extends ToggleSource>({
|
||||
const deviceIcons = useDeviceIcons(kind)
|
||||
const cannotUseDevice = useCannotUseDevice(kind)
|
||||
const deviceShortcut = useDeviceShortcut(kind)
|
||||
const announce = useScreenReaderAnnounce()
|
||||
|
||||
useRegisterKeyboardShortcut({
|
||||
id: deviceShortcut?.id,
|
||||
handler: async () => {
|
||||
const nextState = !enabled
|
||||
try {
|
||||
const didChange = await toggle(nextState)
|
||||
if (didChange === false) return
|
||||
|
||||
const message = t(nextState ? 'turnedOn' : 'turnedOff', {
|
||||
keyPrefix: `selectDevice.${kind}`,
|
||||
})
|
||||
announce(message, 'assertive')
|
||||
} catch {
|
||||
// no announce
|
||||
}
|
||||
},
|
||||
handler: async () => await toggle(),
|
||||
isDisabled: cannotUseDevice,
|
||||
})
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ import { useTranslation } from 'react-i18next'
|
||||
import { useSidePanel } from '../../hooks/useSidePanel'
|
||||
import { css } from '@/styled-system/css'
|
||||
import { ToggleButtonProps } from '@/primitives/ToggleButton'
|
||||
import { useRegisterKeyboardShortcut } from '@/features/shortcuts/useRegisterKeyboardShortcut'
|
||||
|
||||
export const ToolsToggle = ({
|
||||
variant = 'primaryTextDark',
|
||||
@@ -16,11 +15,6 @@ export const ToolsToggle = ({
|
||||
const { isToolsOpen, toggleTools } = useSidePanel()
|
||||
const tooltipLabel = isToolsOpen ? 'open' : 'closed'
|
||||
|
||||
useRegisterKeyboardShortcut({
|
||||
id: 'recording',
|
||||
handler: toggleTools,
|
||||
})
|
||||
|
||||
return (
|
||||
<div
|
||||
className={css({
|
||||
|
||||
@@ -12,7 +12,6 @@ import { StartMediaButton } from '../../components/controls/StartMediaButton'
|
||||
import { MoreOptions } from './MoreOptions'
|
||||
import { useRef } from 'react'
|
||||
import { useRegisterKeyboardShortcut } from '@/features/shortcuts/useRegisterKeyboardShortcut'
|
||||
import { useFullScreen } from '../../hooks/useFullScreen'
|
||||
import { VideoDeviceControl } from '../../components/controls/Device/VideoDeviceControl'
|
||||
import { AudioDevicesControl } from '../../components/controls/Device/AudioDevicesControl'
|
||||
|
||||
@@ -22,8 +21,6 @@ export function DesktopControlBar({
|
||||
const browserSupportsScreenSharing = supportsScreenSharing()
|
||||
const desktopControlBarEl = useRef<HTMLDivElement>(null)
|
||||
|
||||
const { toggleFullScreen, isFullscreenAvailable } = useFullScreen({})
|
||||
|
||||
useRegisterKeyboardShortcut({
|
||||
id: 'focus-toolbar',
|
||||
handler: () => {
|
||||
@@ -35,13 +32,6 @@ export function DesktopControlBar({
|
||||
firstButton?.focus()
|
||||
},
|
||||
})
|
||||
|
||||
useRegisterKeyboardShortcut({
|
||||
id: 'fullscreen',
|
||||
handler: toggleFullScreen,
|
||||
isDisabled: !isFullscreenAvailable,
|
||||
})
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={desktopControlBarEl}
|
||||
|
||||
@@ -31,9 +31,6 @@ import { RecordingProvider } from '@/features/recording'
|
||||
import { ScreenShareErrorModal } from '../components/ScreenShareErrorModal'
|
||||
import { useConnectionObserver } from '../hooks/useConnectionObserver'
|
||||
import { useNoiseReduction } from '../hooks/useNoiseReduction'
|
||||
import { useRegisterKeyboardShortcut } from '@/features/shortcuts/useRegisterKeyboardShortcut'
|
||||
import { useSettingsDialog } from '@/features/settings'
|
||||
import { SettingsDialogExtendedKey } from '@/features/settings/type'
|
||||
import { useVideoResolutionSubscription } from '../hooks/useVideoResolutionSubscription'
|
||||
import { SettingsDialogProvider } from '@/features/settings/components/SettingsDialogProvider'
|
||||
import { useSubtitles } from '@/features/subtitle/hooks/useSubtitles'
|
||||
@@ -100,7 +97,6 @@ export function VideoConference({ ...props }: VideoConferenceProps) {
|
||||
const { t: tRooms } = useTranslation('rooms')
|
||||
const room = useRoomContext()
|
||||
const announce = useScreenReaderAnnounce()
|
||||
const { toggleSettingsDialog } = useSettingsDialog()
|
||||
|
||||
const getAnnouncementName = useCallback(
|
||||
(participant?: Participant | null) => {
|
||||
@@ -115,13 +111,6 @@ export function VideoConference({ ...props }: VideoConferenceProps) {
|
||||
useConnectionObserver()
|
||||
useVideoResolutionSubscription()
|
||||
|
||||
useRegisterKeyboardShortcut({
|
||||
id: 'open-shortcuts',
|
||||
handler: useCallback(() => {
|
||||
toggleSettingsDialog(SettingsDialogExtendedKey.SHORTCUTS)
|
||||
}, [toggleSettingsDialog]),
|
||||
})
|
||||
|
||||
const tracks = useTracks(
|
||||
[
|
||||
{ source: Track.Source.Camera, withPlaceholder: true },
|
||||
|
||||
@@ -4,10 +4,8 @@ export const roomIdPattern = '[a-z]{3}-[a-z]{4}-[a-z]{3}'
|
||||
export const flexibleRoomIdPattern =
|
||||
'(?:[a-zA-Z0-9]{3}-?[a-zA-Z0-9]{4}-?[a-zA-Z0-9]{3})'
|
||||
|
||||
const roomRegex = new RegExp(`^${roomIdPattern}$`)
|
||||
|
||||
export const isRoomValid = (roomIdOrUrl: string) =>
|
||||
roomRegex.test(roomIdOrUrl) ||
|
||||
new RegExp(`^${roomIdPattern}$`).test(roomIdOrUrl) ||
|
||||
new RegExp(`^${window.location.origin}/${roomIdPattern}$`).test(roomIdOrUrl)
|
||||
|
||||
export const normalizeRoomId = (roomId: string) => {
|
||||
|
||||
@@ -3,20 +3,15 @@ import { ShortcutRow } from '@/features/shortcuts/components/ShortcutRow'
|
||||
import { css } from '@/styled-system/css'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { TabPanel, type TabPanelProps } from '@/primitives/Tabs'
|
||||
import { H } from '@/primitives'
|
||||
|
||||
const tableStyle = css({
|
||||
width: '100%',
|
||||
borderCollapse: 'collapse',
|
||||
overflowY: 'auto',
|
||||
'& caption': {
|
||||
fontWeight: 'bold',
|
||||
marginBottom: '0.75rem',
|
||||
textAlign: 'left',
|
||||
},
|
||||
'& th, & td': {
|
||||
padding: '0.65rem 0',
|
||||
textAlign: 'left',
|
||||
fontWeight: 'normal',
|
||||
},
|
||||
'& tbody tr': {
|
||||
borderBottom: '1px solid rgba(255,255,255,0.08)',
|
||||
@@ -34,11 +29,12 @@ export const ShortcutTab = ({ id }: Pick<TabPanelProps, 'id'>) => {
|
||||
className={css({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '0.75rem',
|
||||
})}
|
||||
>
|
||||
<H lvl={2}>{t('shortcuts.listLabel')}</H>
|
||||
<table className={tableStyle}>
|
||||
<caption>{t('shortcuts.listLabel')}</caption>
|
||||
<thead>
|
||||
<thead className="sr-only">
|
||||
<tr>
|
||||
<th scope="col">{t('shortcuts.columnAction')}</th>
|
||||
<th scope="col">{t('shortcuts.columnShortcut')}</th>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { TabPanel, TabPanelProps } from '@/primitives/Tabs'
|
||||
import { useMediaDeviceSelect, useRoomContext } from '@livekit/components-react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { usePersistentUserChoices } from '@/features/rooms/livekit/hooks/usePersistentUserChoices'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { css } from '@/styled-system/css'
|
||||
import {
|
||||
createLocalVideoTrack,
|
||||
@@ -22,8 +22,6 @@ export type VideoTabProps = Pick<DialogProps, 'onOpenChange'> &
|
||||
|
||||
type DeviceItems = Array<{ value: string; label: string }>
|
||||
|
||||
const EMPTY_PROPS = {}
|
||||
|
||||
export const VideoTab = ({ id }: VideoTabProps) => {
|
||||
const { t } = useTranslation('settings', { keyPrefix: 'video' })
|
||||
const { localParticipant, remoteParticipants } = useRoomContext()
|
||||
@@ -61,7 +59,7 @@ export const VideoTab = ({ id }: VideoTabProps) => {
|
||||
const isCamEnabled = devicesIn?.length > 0
|
||||
|
||||
const disabledProps = isCamEnabled
|
||||
? EMPTY_PROPS
|
||||
? {}
|
||||
: {
|
||||
placeholder: t('permissionsRequired'),
|
||||
isDisabled: true,
|
||||
@@ -119,40 +117,6 @@ export const VideoTab = ({ id }: VideoTabProps) => {
|
||||
}
|
||||
}, [videoDeviceId, videoElement])
|
||||
|
||||
const resolutionItems = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
value: 'h720',
|
||||
label: `${t('resolution.publish.items.high')} (720p)`,
|
||||
},
|
||||
{
|
||||
value: 'h360',
|
||||
label: `${t('resolution.publish.items.medium')} (360p)`,
|
||||
},
|
||||
{
|
||||
value: 'h180',
|
||||
label: `${t('resolution.publish.items.low')} (180p)`,
|
||||
},
|
||||
]
|
||||
}, [t])
|
||||
|
||||
const videoQualityItems = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
value: VideoQuality.HIGH.toString(),
|
||||
label: t('resolution.subscribe.items.high'),
|
||||
},
|
||||
{
|
||||
value: VideoQuality.MEDIUM.toString(),
|
||||
label: t('resolution.subscribe.items.medium'),
|
||||
},
|
||||
{
|
||||
value: VideoQuality.LOW.toString(),
|
||||
label: t('resolution.subscribe.items.low'),
|
||||
},
|
||||
]
|
||||
}, [t])
|
||||
|
||||
return (
|
||||
<TabPanel padding={'md'} flex id={id}>
|
||||
<RowWrapper heading={t('camera.heading')}>
|
||||
@@ -214,7 +178,20 @@ export const VideoTab = ({ id }: VideoTabProps) => {
|
||||
<Field
|
||||
type="select"
|
||||
label={t('resolution.publish.label')}
|
||||
items={resolutionItems}
|
||||
items={[
|
||||
{
|
||||
value: 'h720',
|
||||
label: `${t('resolution.publish.items.high')} (720p)`,
|
||||
},
|
||||
{
|
||||
value: 'h360',
|
||||
label: `${t('resolution.publish.items.medium')} (360p)`,
|
||||
},
|
||||
{
|
||||
value: 'h180',
|
||||
label: `${t('resolution.publish.items.low')} (180p)`,
|
||||
},
|
||||
]}
|
||||
selectedKey={videoPublishResolution}
|
||||
onSelectionChange={async (key) => {
|
||||
await handleVideoResolutionChange(key as VideoResolution)
|
||||
@@ -229,7 +206,20 @@ export const VideoTab = ({ id }: VideoTabProps) => {
|
||||
<Field
|
||||
type="select"
|
||||
label={t('resolution.subscribe.label')}
|
||||
items={videoQualityItems}
|
||||
items={[
|
||||
{
|
||||
value: VideoQuality.HIGH.toString(),
|
||||
label: t('resolution.subscribe.items.high'),
|
||||
},
|
||||
{
|
||||
value: VideoQuality.MEDIUM.toString(),
|
||||
label: t('resolution.subscribe.items.medium'),
|
||||
},
|
||||
{
|
||||
value: VideoQuality.LOW.toString(),
|
||||
label: t('resolution.subscribe.items.low'),
|
||||
},
|
||||
]}
|
||||
selectedKey={videoSubscribeQuality?.toString()}
|
||||
onSelectionChange={(key) => {
|
||||
if (key == undefined) return
|
||||
|
||||
@@ -14,25 +14,7 @@ export const useSettingsDialog = () => {
|
||||
settingsStore.areSettingsOpen = true
|
||||
}
|
||||
|
||||
const closeSettingsDialog = () => {
|
||||
settingsStore.areSettingsOpen = false
|
||||
}
|
||||
|
||||
const toggleSettingsDialog = (
|
||||
defaultSelectedTab?: SettingsDialogExtendedKey
|
||||
) => {
|
||||
if (areSettingsOpen) {
|
||||
closeSettingsDialog()
|
||||
} else {
|
||||
if (defaultSelectedTab)
|
||||
settingsStore.defaultSelectedTab = defaultSelectedTab
|
||||
settingsStore.areSettingsOpen = true
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
openSettingsDialog,
|
||||
closeSettingsDialog,
|
||||
toggleSettingsDialog,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import { Shortcut } from './types'
|
||||
export type ShortcutCategory = 'navigation' | 'media' | 'interaction'
|
||||
|
||||
export type ShortcutId =
|
||||
| 'open-shortcuts'
|
||||
| 'focus-toolbar'
|
||||
| 'toggle-microphone'
|
||||
| 'toggle-camera'
|
||||
@@ -30,11 +29,6 @@ export type ShortcutDescriptor = {
|
||||
}
|
||||
|
||||
export const shortcutCatalog: ShortcutDescriptor[] = [
|
||||
{
|
||||
id: 'open-shortcuts',
|
||||
category: 'navigation',
|
||||
shortcut: { key: '/', ctrlKey: true, shiftKey: true },
|
||||
},
|
||||
{
|
||||
id: 'focus-toolbar',
|
||||
category: 'navigation',
|
||||
|
||||
@@ -25,9 +25,9 @@ export const ShortcutBadge: React.FC<ShortcutBadgeProps> = ({
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<kbd className={cx(badgeStyle, className)} aria-hidden="true">
|
||||
{visualLabel}
|
||||
</kbd>
|
||||
<div className={cx(badgeStyle, className)} aria-hidden="true">
|
||||
<span>{visualLabel}</span>
|
||||
</div>
|
||||
{srLabel && <span className="sr-only">{srLabel}</span>}
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -31,9 +31,9 @@ export const ShortcutRow: React.FC<ShortcutRowProps> = ({ descriptor }) => {
|
||||
|
||||
return (
|
||||
<tr>
|
||||
<th scope="row" className={text({ variant: 'body' })}>
|
||||
<td className={text({ variant: 'body' })}>
|
||||
{t(`actions.${descriptor.id}`)}
|
||||
</th>
|
||||
</td>
|
||||
<td className={shortcutCellStyle}>
|
||||
<ShortcutBadge visualLabel={visualShortcut} srLabel={srShortcut} />
|
||||
</td>
|
||||
|
||||
@@ -19,10 +19,7 @@ export const useKeyboardShortcuts = () => {
|
||||
shiftKey,
|
||||
altKey,
|
||||
})
|
||||
let shortcut = shortcutsSnap.shortcuts.get(shortcutKey)
|
||||
if (!shortcut && shortcutKey === 'ctrl+shift+?') {
|
||||
shortcut = shortcutsSnap.shortcuts.get('ctrl+shift+/')
|
||||
}
|
||||
const shortcut = shortcutsSnap.shortcuts.get(shortcutKey)
|
||||
if (!shortcut) return
|
||||
e.preventDefault()
|
||||
await shortcut()
|
||||
|
||||
@@ -5,7 +5,6 @@ import { layoutStore } from '@/stores/layout'
|
||||
import { useSnapshot } from 'valtio'
|
||||
import { Footer } from '@/layout/Footer'
|
||||
import { ScreenReaderAnnouncer } from '@/primitives'
|
||||
import { SkipLink, MAIN_CONTENT_ID } from './SkipLink'
|
||||
|
||||
export type Layout = 'fullpage' | 'centered'
|
||||
|
||||
@@ -22,7 +21,6 @@ export const Layout = ({ children }: { children: ReactNode }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
{showHeader && <SkipLink />}
|
||||
<div
|
||||
className={css({
|
||||
display: 'flex',
|
||||
@@ -37,7 +35,6 @@ export const Layout = ({ children }: { children: ReactNode }) => {
|
||||
>
|
||||
{showHeader && <Header />}
|
||||
<main
|
||||
id={MAIN_CONTENT_ID}
|
||||
className={css({
|
||||
flexGrow: 1,
|
||||
overflow: 'auto',
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
import { type MouseEvent } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { styled } from '@/styled-system/jsx'
|
||||
|
||||
export const MAIN_CONTENT_ID = 'main-content'
|
||||
|
||||
// Visually hidden until focus (not sr-only). Must become visible on focus for keyboard users.
|
||||
const StyledSkipLink = styled('a', {
|
||||
base: {
|
||||
position: 'absolute',
|
||||
width: '1px',
|
||||
height: '1px',
|
||||
margin: '-1px',
|
||||
padding: 0,
|
||||
overflow: 'hidden',
|
||||
clip: 'rect(0, 0, 0, 0)',
|
||||
whiteSpace: 'nowrap',
|
||||
border: 0,
|
||||
textDecoration: 'none',
|
||||
_focusVisible: {
|
||||
position: 'fixed',
|
||||
top: '0.5rem',
|
||||
left: '50%',
|
||||
transform: 'translateX(-50%)',
|
||||
width: 'auto',
|
||||
height: 'auto',
|
||||
margin: 0,
|
||||
padding: '0.625rem 1rem',
|
||||
overflow: 'visible',
|
||||
clip: 'auto',
|
||||
whiteSpace: 'normal',
|
||||
zIndex: 9999,
|
||||
backgroundColor: 'white',
|
||||
color: 'primary.800',
|
||||
fontWeight: 500,
|
||||
fontSize: '0.875rem',
|
||||
border: '1px solid',
|
||||
borderColor: 'primary.800',
|
||||
borderRadius: 4,
|
||||
outline: '2px solid',
|
||||
outlineColor: 'focusRing',
|
||||
outlineOffset: 2,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export const SkipLink = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const handleClick = (e: MouseEvent<HTMLAnchorElement>) => {
|
||||
e.preventDefault()
|
||||
const main = document.getElementById(MAIN_CONTENT_ID)
|
||||
if (!main) return
|
||||
|
||||
const heading = main.querySelector('h1, h2, h3') as HTMLElement | null
|
||||
const target = heading ?? main
|
||||
|
||||
if (!target.hasAttribute('tabindex')) {
|
||||
target.setAttribute('tabindex', '-1')
|
||||
}
|
||||
target.focus()
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledSkipLink href={`#${MAIN_CONTENT_ID}`} onClick={handleClick}>
|
||||
{t('skipLink')}
|
||||
</StyledSkipLink>
|
||||
)
|
||||
}
|
||||
@@ -52,7 +52,6 @@
|
||||
"label": "OK"
|
||||
}
|
||||
},
|
||||
"skipLink": "Zum Hauptinhalt springen",
|
||||
"clipboardContent": {
|
||||
"url": "Um an der Videokonferenz teilzunehmen, klicken Sie auf diesen Link: {{roomUrl}}",
|
||||
"numberAndPin": "Um telefonisch teilzunehmen, wählen Sie {{phoneNumber}} und geben Sie diesen Code ein: {{pinCode}}"
|
||||
|
||||
@@ -31,14 +31,12 @@
|
||||
},
|
||||
"introSlider": {
|
||||
"previous": {
|
||||
"label": "Vorherige Folie",
|
||||
"labelWithPosition": "Vorherige Folie ({{current}} von {{total}})",
|
||||
"tooltip": "Vorherige Folie"
|
||||
"label": "Zurück",
|
||||
"tooltip": "Zurück"
|
||||
},
|
||||
"next": {
|
||||
"label": "Nächste Folie",
|
||||
"labelWithPosition": "Nächste Folie ({{current}} von {{total}})",
|
||||
"tooltip": "Nächste Folie"
|
||||
"label": "Weiter",
|
||||
"tooltip": "Weiter"
|
||||
},
|
||||
"beta": {
|
||||
"text": "An der Beta teilnehmen",
|
||||
@@ -55,8 +53,6 @@
|
||||
"slide3": {
|
||||
"title": "Verwandeln Sie Ihre Meetings mit KI",
|
||||
"body": "Erhalten Sie präzise und verwertbare Transkripte zur Steigerung Ihrer Produktivität. Funktion in der Beta – jetzt testen!"
|
||||
},
|
||||
"carouselLabel": "Einführungs-Diashow",
|
||||
"slidePosition": "Folie {{current}} von {{total}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,8 +22,6 @@
|
||||
"permissionsNeeded": "Kamera auswählen - genehmigung erforderlich",
|
||||
"disable": "Kamera deaktivieren",
|
||||
"enable": "Kamera aktivieren",
|
||||
"turnedOff": "Kamera deaktiviert",
|
||||
"turnedOn": "Kamera aktiviert",
|
||||
"label": "Kamera",
|
||||
"placeholder": "Kamera aktivieren, um die Vorschau zu sehen"
|
||||
},
|
||||
@@ -32,8 +30,6 @@
|
||||
"permissionsNeeded": "Mikrofon auswählen - genehmigung erforderlich",
|
||||
"disable": "Mikrofon deaktivieren",
|
||||
"enable": "Mikrofon aktivieren",
|
||||
"turnedOff": "Mikrofon deaktiviert",
|
||||
"turnedOn": "Mikrofon aktiviert",
|
||||
"label": "Mikrofon"
|
||||
},
|
||||
"audiooutput": {
|
||||
@@ -590,7 +586,7 @@
|
||||
},
|
||||
"participantTileFocus": {
|
||||
"containerLabel": "Optionen für {{name}}",
|
||||
"toolbarHint": "Ctrl+Shift+/: Direkt auf die Tastenkürzel zugreifen.",
|
||||
"toolbarHint": "F2: zur Symbolleiste unten.",
|
||||
"pin": {
|
||||
"enable": "Anheften",
|
||||
"disable": "Lösen"
|
||||
|
||||
@@ -52,7 +52,6 @@
|
||||
"label": "OK"
|
||||
}
|
||||
},
|
||||
"skipLink": "Skip to main content",
|
||||
"clipboardContent": {
|
||||
"url": "To join the video conference, click on this link: {{roomUrl}}",
|
||||
"numberAndPin": "To join by phone, dial {{phoneNumber}} and enter this code: {{pinCode}}"
|
||||
|
||||
@@ -31,14 +31,12 @@
|
||||
},
|
||||
"introSlider": {
|
||||
"previous": {
|
||||
"label": "Previous slide",
|
||||
"labelWithPosition": "Previous slide ({{current}} of {{total}})",
|
||||
"tooltip": "Previous slide"
|
||||
"label": "previous",
|
||||
"tooltip": "previous"
|
||||
},
|
||||
"next": {
|
||||
"label": "Next slide",
|
||||
"labelWithPosition": "Next slide ({{current}} of {{total}})",
|
||||
"tooltip": "Next slide"
|
||||
"label": "next",
|
||||
"tooltip": "next"
|
||||
},
|
||||
"beta": {
|
||||
"text": "Join the beta",
|
||||
@@ -55,8 +53,6 @@
|
||||
"slide3": {
|
||||
"title": "Transform your meetings with AI",
|
||||
"body": "Get accurate and actionable transcripts to boost your productivity. Feature in beta—try it now!"
|
||||
},
|
||||
"carouselLabel": "Introduction slideshow",
|
||||
"slidePosition": "Slide {{current}} of {{total}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,8 +22,6 @@
|
||||
"permissionsNeeded": "Select camera - permission needed",
|
||||
"disable": "Disable camera",
|
||||
"enable": "Enable camera",
|
||||
"turnedOff": "Camera turned off",
|
||||
"turnedOn": "Camera turned on",
|
||||
"label": "Camera",
|
||||
"placeholder": "Enable camera to see the preview"
|
||||
},
|
||||
@@ -32,8 +30,6 @@
|
||||
"permissionsNeeded": "Select microphone - permission needed",
|
||||
"disable": "Disable microphone",
|
||||
"enable": "Enable microphone",
|
||||
"turnedOff": "Microphone turned off",
|
||||
"turnedOn": "Microphone turned on",
|
||||
"label": "Microphone"
|
||||
},
|
||||
"audiooutput": {
|
||||
@@ -590,7 +586,7 @@
|
||||
},
|
||||
"participantTileFocus": {
|
||||
"containerLabel": "Options for {{name}}",
|
||||
"toolbarHint": "Ctrl+Shift+/: access shortcuts directly.",
|
||||
"toolbarHint": "F2: go to the bottom toolbar.",
|
||||
"pin": {
|
||||
"enable": "Pin",
|
||||
"disable": "Unpin"
|
||||
|
||||
@@ -52,7 +52,6 @@
|
||||
"label": "OK"
|
||||
}
|
||||
},
|
||||
"skipLink": "Aller au contenu principal",
|
||||
"clipboardContent": {
|
||||
"url": "Pour participer à la visioconférence, cliquez sur ce lien : {{roomUrl}}",
|
||||
"numberAndPin": "Pour participer par téléphone, composez le {{phoneNumber}} et saisissez ce code : {{pinCode}}"
|
||||
|
||||
@@ -30,18 +30,14 @@
|
||||
}
|
||||
},
|
||||
"introSlider": {
|
||||
"carouselLabel": "Diaporama de présentation",
|
||||
"previous": {
|
||||
"label": "Diapositive précédente",
|
||||
"labelWithPosition": "Diapositive précédente ({{current}} sur {{total}})",
|
||||
"tooltip": "Diapositive précédente"
|
||||
"label": "précédent",
|
||||
"tooltip": "précédent"
|
||||
},
|
||||
"next": {
|
||||
"label": "Diapositive suivante",
|
||||
"labelWithPosition": "Diapositive suivante ({{current}} sur {{total}})",
|
||||
"tooltip": "Diapositive suivante"
|
||||
"label": "suivant",
|
||||
"tooltip": "suivant"
|
||||
},
|
||||
"slidePosition": "Diapositive {{current}} sur {{total}}",
|
||||
"beta": {
|
||||
"text": "Essayer la beta",
|
||||
"tooltip": "Accéder au formulaire"
|
||||
|
||||
@@ -22,8 +22,6 @@
|
||||
"permissionsNeeded": "Choisir la webcam - autorisations nécessaires",
|
||||
"disable": "Désactiver la webcam",
|
||||
"enable": "Activer la webcam",
|
||||
"turnedOff": "Webcam désactivée",
|
||||
"turnedOn": "Webcam activée",
|
||||
"label": "Webcam",
|
||||
"placeholder": "Activez la webcam pour prévisualiser l'affichage"
|
||||
},
|
||||
@@ -32,8 +30,6 @@
|
||||
"permissionsNeeded": "Choisir le micro - autorisations nécessaires",
|
||||
"disable": "Désactiver le micro",
|
||||
"enable": "Activer le micro",
|
||||
"turnedOff": "Micro désactivé",
|
||||
"turnedOn": "Micro activé",
|
||||
"label": "Microphone"
|
||||
},
|
||||
"audiooutput": {
|
||||
@@ -590,7 +586,7 @@
|
||||
},
|
||||
"participantTileFocus": {
|
||||
"containerLabel": "Options pour {{name}}",
|
||||
"toolbarHint": "Ctrl+Shift+/ : accéder directement aux raccourcis.",
|
||||
"toolbarHint": "F2 : raccourci barre d'outils en bas.",
|
||||
"pin": {
|
||||
"enable": "Épingler",
|
||||
"disable": "Annuler l'épinglage"
|
||||
|
||||
@@ -51,7 +51,6 @@
|
||||
"label": "OK"
|
||||
}
|
||||
},
|
||||
"skipLink": "Naar de hoofdinhoud gaan",
|
||||
"clipboardContent": {
|
||||
"url": "Klik op deze link om deel te nemen aan de videoconferentie: {{roomUrl}}",
|
||||
"numberAndPin": "Bel {{phoneNumber}} en voer deze code in om telefonisch deel te nemen: {{pinCode}}"
|
||||
|
||||
@@ -31,14 +31,12 @@
|
||||
},
|
||||
"introSlider": {
|
||||
"previous": {
|
||||
"label": "Vorige dia",
|
||||
"labelWithPosition": "Vorige dia ({{current}} van {{total}})",
|
||||
"tooltip": "Vorige dia"
|
||||
"label": "vorige",
|
||||
"tooltip": "vorige"
|
||||
},
|
||||
"next": {
|
||||
"label": "Volgende dia",
|
||||
"labelWithPosition": "Volgende dia ({{current}} van {{total}})",
|
||||
"tooltip": "Volgende dia"
|
||||
"label": "volgende",
|
||||
"tooltip": "volgende"
|
||||
},
|
||||
"beta": {
|
||||
"text": "Word lid van de bèta",
|
||||
@@ -55,8 +53,6 @@
|
||||
"slide3": {
|
||||
"title": "Transformeer uw vergaderingen met AI",
|
||||
"body": "Krijg nauwkeurige en bruikbare transcripties om uw productiviteit te stimuleren. Deze mogelijkheid is in bèta, probeer het nu!"
|
||||
},
|
||||
"carouselLabel": "Introductie-diavoorstelling",
|
||||
"slidePosition": "Dia {{current}} van {{total}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,8 +22,6 @@
|
||||
"permissionsNeeded": "Selecteer camera - Toestemming vereist",
|
||||
"disable": "Camera uitschakelen",
|
||||
"enable": "Camera inschakelen",
|
||||
"turnedOff": "Camera uitgeschakeld",
|
||||
"turnedOn": "Camera ingeschakeld",
|
||||
"label": "Camera",
|
||||
"placeholder": "Schakel de camera in om de preview te zien"
|
||||
},
|
||||
@@ -32,8 +30,6 @@
|
||||
"permissionsNeeded": "Selecteer microfoon - Toestemming vereist",
|
||||
"disable": "Microfoon dempen",
|
||||
"enable": "Microfoon dempen opheffen",
|
||||
"turnedOff": "Microfoon uitgeschakeld",
|
||||
"turnedOn": "Microfoon ingeschakeld",
|
||||
"label": "Microfoon"
|
||||
},
|
||||
"audiooutput": {
|
||||
@@ -590,7 +586,7 @@
|
||||
},
|
||||
"participantTileFocus": {
|
||||
"containerLabel": "Opties voor {{name}}",
|
||||
"toolbarHint": "Ctrl+Shift+/: direct toegang tot de sneltoetsen.",
|
||||
"toolbarHint": "F2: naar de werkbalk onderaan.",
|
||||
"pin": {
|
||||
"enable": "Pinnen",
|
||||
"disable": "Losmaken"
|
||||
|
||||
@@ -11,8 +11,6 @@ import { CreatePopup } from '@/features/sdk/routes/CreatePopup'
|
||||
import { CreateMeetingButton } from '@/features/sdk/routes/CreateMeetingButton'
|
||||
import { RecordingDownloadRoute } from '@/features/recording'
|
||||
|
||||
const roomIdRegex = new RegExp(`^[/](?<roomId>${flexibleRoomIdPattern})$`)
|
||||
|
||||
export const routes: Record<
|
||||
| 'home'
|
||||
| 'room'
|
||||
@@ -39,7 +37,7 @@ export const routes: Record<
|
||||
room: {
|
||||
name: 'room',
|
||||
to: (roomId: string) => `/${roomId.trim()}`,
|
||||
path: roomIdRegex,
|
||||
path: new RegExp(`^[/](?<roomId>${flexibleRoomIdPattern})$`),
|
||||
Component: RoomRoute,
|
||||
},
|
||||
feedback: {
|
||||
|
||||
@@ -22,11 +22,6 @@ body,
|
||||
outline: 2px solid transparent;
|
||||
}
|
||||
|
||||
main#main-content :is(h1, h2, h3)[tabindex='-1']:focus {
|
||||
outline: 2px solid var(--colors-focus-ring);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
[data-rac][data-focus-visible]:not(label, .react-aria-Select),
|
||||
:is(a, button, input[type='text'], select, textarea):not(
|
||||
[data-rac]
|
||||
|
||||
@@ -151,37 +151,3 @@
|
||||
[data-lk-theme] .lk-participant-tile {
|
||||
box-shadow: var(--lk-box-shadow);
|
||||
}
|
||||
|
||||
/* Participant name ellipsis: truncate when overflowing */
|
||||
.lk-participant-metadata {
|
||||
width: 100%;
|
||||
}
|
||||
.lk-participant-metadata > *:first-child {
|
||||
min-width: 0;
|
||||
}
|
||||
.lk-participant-metadata > *:first-child {
|
||||
flex: 1;
|
||||
}
|
||||
.lk-participant-metadata > *:first-child .lk-participant-metadata-item,
|
||||
.lk-participant-metadata
|
||||
.lk-participant-metadata-item
|
||||
.lk-participant-name-wrapper {
|
||||
min-width: 0;
|
||||
}
|
||||
.lk-participant-metadata > *:first-child .lk-participant-metadata-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Shortcut hint: visible only on first grid tile when focused (CSS-based) */
|
||||
.shortcut-hint-wrapper {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
transition: opacity 150ms ease;
|
||||
}
|
||||
.lk-grid-layout > *:first-child:focus-within .shortcut-hint-wrapper {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
// Map frontend language codes to backend language codes
|
||||
|
||||
export type BackendLanguage = 'en-us' | 'fr-fr' | 'nl-nl' | 'de-de'
|
||||
export type FrontendLanguage = 'en' | 'fr' | 'nl' | 'de'
|
||||
export type BackendLanguage = 'en-us' | 'fr-fr' | 'nl-nl'
|
||||
export type FrontendLanguage = 'en' | 'fr' | 'nl'
|
||||
|
||||
const frontendToBackendMap: Record<FrontendLanguage, BackendLanguage> = {
|
||||
en: 'en-us',
|
||||
fr: 'fr-fr',
|
||||
nl: 'nl-nl',
|
||||
de: 'de-de',
|
||||
}
|
||||
|
||||
export const convertToBackendLanguage = (
|
||||
|
||||
@@ -128,10 +128,6 @@ ingressAdmin:
|
||||
enabled: true
|
||||
host: meet.127.0.0.1.nip.io
|
||||
|
||||
ingressWebhook:
|
||||
enabled: true
|
||||
host: meet.127.0.0.1.nip.io
|
||||
|
||||
posthog:
|
||||
ingress:
|
||||
enabled: false
|
||||
|
||||
@@ -79,6 +79,7 @@ backend:
|
||||
APPLICATION_JWT_AUDIENCE: https://meet.127.0.0.1.nip.io/external-api/v1.0/
|
||||
APPLICATION_JWT_SECRET_KEY: devKeyApplication
|
||||
APPLICATION_BASE_URL: https://meet.127.0.0.1.nip.io
|
||||
ADDONS_JWT_SECRET_KEY: devKeyApplicationAddons
|
||||
|
||||
|
||||
migrate:
|
||||
@@ -141,10 +142,6 @@ ingressAdmin:
|
||||
enabled: true
|
||||
host: meet.127.0.0.1.nip.io
|
||||
|
||||
ingressWebhook:
|
||||
enabled: true
|
||||
host: meet.127.0.0.1.nip.io
|
||||
|
||||
posthog:
|
||||
ingress:
|
||||
enabled: false
|
||||
|
||||
@@ -156,10 +156,6 @@ ingressAdmin:
|
||||
enabled: true
|
||||
host: meet.127.0.0.1.nip.io
|
||||
|
||||
ingressWebhook:
|
||||
enabled: true
|
||||
host: meet.127.0.0.1.nip.io
|
||||
|
||||
posthog:
|
||||
ingress:
|
||||
enabled: false
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
apiVersion: v2
|
||||
type: application
|
||||
name: meet
|
||||
version: 0.0.16
|
||||
version: 0.0.15
|
||||
|
||||
@@ -88,6 +88,20 @@ spec:
|
||||
serviceName: {{ include "meet.backend.fullname" . }}
|
||||
servicePort: {{ .Values.backend.service.port }}
|
||||
{{- end }}
|
||||
- path: /addons/
|
||||
{{- if semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion }}
|
||||
pathType: Prefix
|
||||
{{- end }}
|
||||
backend:
|
||||
{{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }}
|
||||
service:
|
||||
name: {{ include "meet.backend.fullname" . }}
|
||||
port:
|
||||
number: {{ .Values.backend.service.port }}
|
||||
{{- else }}
|
||||
serviceName: {{ include "meet.backend.fullname" . }}
|
||||
servicePort: {{ .Values.backend.service.port }}
|
||||
{{- end }}
|
||||
{{- with .Values.ingress.customBackends }}
|
||||
{{- toYaml . | nindent 10 }}
|
||||
{{- end }}
|
||||
@@ -138,6 +152,20 @@ spec:
|
||||
serviceName: {{ include "meet.backend.fullname" $ }}
|
||||
servicePort: {{ $.Values.backend.service.port }}
|
||||
{{- end }}
|
||||
- path: /addons/
|
||||
{{- if semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion }}
|
||||
pathType: Prefix
|
||||
{{- end }}
|
||||
backend:
|
||||
{{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }}
|
||||
service:
|
||||
name: {{ include "meet.backend.fullname" $ }}
|
||||
port:
|
||||
number: {{ $.Values.backend.service.port }}
|
||||
{{- else }}
|
||||
serviceName: {{ include "meet.backend.fullname" $ }}
|
||||
servicePort: {{ $.Values.backend.service.port }}
|
||||
{{- end }}
|
||||
{{- with $.Values.ingress.customBackends }}
|
||||
{{- toYaml . | nindent 10 }}
|
||||
{{- end }}
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
{{- if .Values.ingressWebhook.enabled -}}
|
||||
{{- $fullName := include "meet.fullname" . -}}
|
||||
{{- if and .Values.ingressWebhook.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }}
|
||||
{{- if not (hasKey .Values.ingressWebhook.annotations "kubernetes.io/ingress.class") }}
|
||||
{{- $_ := set .Values.ingressWebhook.annotations "kubernetes.io/ingress.class" .Values.ingressWebhook.className}}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}}
|
||||
apiVersion: networking.k8s.io/v1
|
||||
{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}}
|
||||
apiVersion: networking.k8s.io/v1beta1
|
||||
{{- else -}}
|
||||
apiVersion: extensions/v1beta1
|
||||
{{- end }}
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: {{ $fullName }}-webhook
|
||||
namespace: {{ .Release.Namespace | quote }}
|
||||
labels:
|
||||
{{- include "meet.labels" . | nindent 4 }}
|
||||
{{- with .Values.ingressWebhook.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
{{- if and .Values.ingressWebhook.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }}
|
||||
ingressClassName: {{ .Values.ingressWebhook.className }}
|
||||
{{- end }}
|
||||
{{- if .Values.ingressWebhook.tls.enabled }}
|
||||
tls:
|
||||
{{- if .Values.ingressWebhook.host }}
|
||||
- secretName: {{ .Values.ingressWebhook.tls.secretName | default (printf "%s-tls" $fullName) | quote }}
|
||||
hosts:
|
||||
- {{ .Values.ingressWebhook.host | quote }}
|
||||
{{- end }}
|
||||
{{- range .Values.ingressWebhook.tls.additional }}
|
||||
- hosts:
|
||||
{{- range .hosts }}
|
||||
- {{ . | quote }}
|
||||
{{- end }}
|
||||
secretName: {{ .secretName }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
rules:
|
||||
{{- if .Values.ingressWebhook.host }}
|
||||
- host: {{ .Values.ingressWebhook.host | quote }}
|
||||
http:
|
||||
paths:
|
||||
- path: {{ .Values.ingressWebhook.path }}
|
||||
{{- if semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion }}
|
||||
pathType: Exact
|
||||
{{- end }}
|
||||
backend:
|
||||
{{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }}
|
||||
service:
|
||||
name: {{ include "meet.backend.fullname" . }}
|
||||
port:
|
||||
number: {{ .Values.backend.service.port }}
|
||||
{{- else }}
|
||||
serviceName: {{ include "meet.backend.fullname" . }}
|
||||
servicePort: {{ .Values.backend.service.port }}
|
||||
{{- end }}
|
||||
{{- with .Values.ingressWebhook.customBackends }}
|
||||
{{- toYaml . | nindent 10 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- range .Values.ingressWebhook.hosts }}
|
||||
- host: {{ . | quote }}
|
||||
http:
|
||||
paths:
|
||||
- path: {{ .Values.ingressWebhook.path }}
|
||||
{{- if semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion }}
|
||||
pathType: Exact
|
||||
{{- end }}
|
||||
backend:
|
||||
{{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }}
|
||||
service:
|
||||
name: {{ include "meet.backend.fullname" $ }}
|
||||
port:
|
||||
number: {{ $.Values.backend.service.port }}
|
||||
{{- else }}
|
||||
serviceName: {{ include "meet.backend.fullname" $ }}
|
||||
servicePort: {{ $.Values.backend.service.port }}
|
||||
{{- end }}
|
||||
{{- with $.Values.ingressWebhook.customBackends }}
|
||||
{{- toYaml . | nindent 10 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
@@ -50,31 +50,6 @@ ingress:
|
||||
## @param ingress.customBackends Add custom backends to ingress
|
||||
customBackends: []
|
||||
|
||||
## @param ingressWebhook.enabled whether to enable the Ingress or not
|
||||
## @param ingressWebhook.className IngressClass to use for the Ingress
|
||||
## @param ingressWebhook.host Host for the Ingress
|
||||
## @param ingressWebhook.path Path to use for the Ingress
|
||||
ingressWebhook:
|
||||
enabled: false
|
||||
className: null
|
||||
host: meet.example.com
|
||||
path: /api/v1.0/rooms/webhooks-livekit/
|
||||
## @param ingressWebhook.hosts Additional host to configure for the Ingress
|
||||
hosts: []
|
||||
# - chart-example.local
|
||||
## @param ingressWebhook.tls.enabled Weather to enable TLS for the Ingress
|
||||
## @param ingressWebhook.tls.secretName Secret name for TLS config
|
||||
## @skip ingressWebhook.tls.additional
|
||||
## @extra ingressWebhook.tls.additional[].secretName Secret name for additional TLS config
|
||||
## @extra ingressWebhook.tls.additional[].hosts[] Hosts for additional TLS config
|
||||
tls:
|
||||
secretName: null
|
||||
enabled: true
|
||||
additional: []
|
||||
|
||||
## @param ingressWebhook.customBackends Add custom backends to ingress
|
||||
customBackends: []
|
||||
|
||||
|
||||
## @param ingressAdmin.enabled whether to enable the Ingress or not
|
||||
## @param ingressAdmin.className IngressClass to use for the Ingress
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user