Compare commits

...

2 Commits

Author SHA1 Message Date
lebaudantoine 6f8f5d0386 🐛(frontend) accept meeting links with query params in join dialog
Meeting links inserted into events by the Outlook calendar add-in carry
tracking query parameters, e.g. `?from=outlook-addin` or `utm_*`.
Pasting such a link into the "Join a meeting" dialog failed: the
validation regex was anchored on the room id, so any trailing query
string made the link invalid, and the submit handler only stripped the
origin, so the query string would have ended up in the room id.

Parse the input with the URL API in a new `extractRoomId` helper, used
by both validation and submit. It accepts a bare room id or a link on
the current origin (trailing slash allowed), reads the room id from the
pathname only, and returns the query string and fragment separately so
they are kept when redirecting to the room (utm tracking is preserved).
2026-11-12 23:50:05 +01:00
lebaudantoine 25122deba2 🔒️(docker) move frontend to nginx/nginx-unprivileged image
The nginxinc/nginx-unprivileged image is no longer rebuilt and
ships libxslt 1.1.43-r3, affected by CVE-2026-125350,
CVE-2026-87636 and CVE-2026-9947.

Use nginx/nginx-unprivileged in both frontend images. Its
packages are up to date, so drop the manual package upgrades.
2026-11-12 23:29:14 +01:00
7 changed files with 75 additions and 21 deletions
+2
View File
@@ -37,6 +37,8 @@ and this project adheres to
- 🔒️(agents) upgrade python to 3.14.10 to fix CVE-2026-109518
- 🔒️(docker) upgrade musl-utils to 1.2.6-r3 to fix CVE-2026-107891
- ♿️(frontend) close the invite dialog with the Escape key #1916
- 🔒️(docker) move frontend to nginx/nginx-unprivileged image
- 🐛(frontend) accept meeting links with query params in join dialog
## [1.37.0] - 2026-11-01
+2 -8
View File
@@ -58,19 +58,13 @@ RUN npx webpack --mode production
# ---- Front-end image ----
FROM nginxinc/nginx-unprivileged:1.30.6-alpine3.24 AS frontend-production
FROM nginx/nginx-unprivileged:1.30.6-alpine3.24 AS frontend-production
USER root
RUN apk upgrade --no-cache libexpat && \
apk add --no-cache --upgrade 'pcre2>=10.49-r0' 'tiff>=4.7.2-r0' \
'libx11>=1.8.14-r0' 'ca-certificates>=20261020-r0' \
'brotli-libs>=1.2.1-r0' 'musl-utils>=1.2.6-r3' && \
apk del curl && \
RUN apk del curl && \
rm -f /usr/share/nginx/html/50x.html
USER nginx
USER nginx
# Un-privileged user running the application
ARG DOCKER_USER
USER ${DOCKER_USER}
+2 -6
View File
@@ -47,14 +47,10 @@ ENV VITE_MEDIA_BASE_URL=${VITE_MEDIA_BASE_URL}
RUN npm run build
# ---- Front-end image ----
FROM nginxinc/nginx-unprivileged:1.30.6-alpine3.24 AS frontend-production
FROM nginx/nginx-unprivileged:1.30.6-alpine3.24 AS frontend-production
USER root
RUN apk upgrade --no-cache libexpat && \
apk add --no-cache --upgrade 'pcre2>=10.49-r0' 'tiff>=4.7.2-r0' \
'libx11>=1.8.14-r0' 'ca-certificates>=20261020-r0' \
'brotli-libs>=1.2.1-r0' 'musl-utils>=1.2.6-r3' && \
apk del curl && \
RUN apk del curl && \
rm -f /usr/share/nginx/html/50x.html
USER nginx
@@ -1,22 +1,21 @@
import { useTranslation } from 'react-i18next'
import { Field, Ul, H, P, Form, Dialog } from '@/primitives'
import { navigateTo } from '@/navigation/navigateTo'
import { isRoomValid } from '@/features/rooms'
import { extractRoomId } 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 extracted = extractRoomId(data.roomId as string)
if (!extracted) return
navigateTo('room', `${extracted.roomId}${extracted.suffix}`)
}
const validateRoomId = (value: string) => {
const trimmed = value.trim()
if (!trimmed) return null
return !isRoomValid(trimmed) ? (
return !extractRoomId(trimmed) ? (
<>
<p>{t('joinInputError')}</p>
<Ul>
+1
View File
@@ -1,6 +1,7 @@
export {
roomIdPattern,
isRoomValid,
extractRoomId,
flexibleRoomIdPattern,
} from './utils/isRoomValid'
export { generateRoomId } from './utils/generateRoomId'
@@ -1,5 +1,45 @@
import { describe, expect, it } from 'vitest'
import { normalizeRoomId } from './isRoomValid'
import { extractRoomId, normalizeRoomId } from './isRoomValid'
describe('extractRoomId', () => {
const origin = 'https://meet.example.com'
const plain = { roomId: 'abc-defg-hij', suffix: '' }
it('accepts a bare room id', () => {
expect(extractRoomId('abc-defg-hij', origin)).toEqual(plain)
expect(extractRoomId(' abc-defg-hij ', origin)).toEqual(plain)
})
it('accepts a meeting URL on this origin', () => {
expect(extractRoomId(`${origin}/abc-defg-hij`, origin)).toEqual(plain)
expect(extractRoomId(`${origin}/abc-defg-hij/`, origin)).toEqual(plain)
})
it('keeps query strings and fragments in the suffix', () => {
expect(
extractRoomId(`${origin}/abc-defg-hij?from=outlook-addin`, origin)
).toEqual({ roomId: 'abc-defg-hij', suffix: '?from=outlook-addin' })
expect(
extractRoomId(
`${origin}/abc-defg-hij/?utm_source=mail&utm_medium=email#x`,
origin
)
).toEqual({
roomId: 'abc-defg-hij',
suffix: '?utm_source=mail&utm_medium=email#x',
})
})
it('rejects other origins and invalid ids', () => {
expect(
extractRoomId('https://evil.example/abc-defg-hij', origin)
).toBeNull()
expect(extractRoomId(`${origin}/not-a-room`, origin)).toBeNull()
expect(extractRoomId(`${origin}/abc-defg-hij/extra`, origin)).toBeNull()
expect(extractRoomId('abc-defg-hij?from=x', origin)).toBeNull()
})
})
describe('normalizeRoomId', () => {
it('lowercases and re-inserts the hyphens of a ten-letter id', () => {
@@ -10,6 +10,28 @@ export const isRoomValid = (roomIdOrUrl: string) =>
roomRegex.test(roomIdOrUrl) ||
new RegExp(`^${window.location.origin}/${roomIdPattern}$`).test(roomIdOrUrl)
export const extractRoomId = (
input: string,
origin: string = window.location.origin
): { roomId: string; suffix: string } | null => {
const value = input.trim()
if (roomRegex.test(value)) return { roomId: value, suffix: '' }
let url: URL
try {
url = new URL(value)
} catch {
return null
}
if (url.origin !== origin) return null
const path = url.pathname.slice(1)
const roomId = path.endsWith('/') ? path.slice(0, -1) : path
return roomRegex.test(roomId)
? { roomId, suffix: `${url.search}${url.hash}` }
: null
}
export const normalizeRoomId = (roomId: string) => {
const cleanId = roomId.toLowerCase().replace(/-/g, '')
if (cleanId.length === 10) {