diff --git a/app/api/jitsi/token/route.ts b/app/api/jitsi/token/route.ts new file mode 100644 index 00000000..05f56de9 --- /dev/null +++ b/app/api/jitsi/token/route.ts @@ -0,0 +1,77 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { cookies } from 'next/headers'; +import { createHmac } from 'node:crypto'; +import { decryptSession } from '@/lib/auth/crypto'; +import { sessionCookieName } from '@/lib/auth/session-cookie'; +import { logger } from '@/lib/logger'; + +const JITSI_URL = (process.env.JITSI_URL || 'https://meet.src-advisory.com').replace(/\/+$/, ''); + +function base64url(input: Buffer): string { + return input.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); +} + +function b64u(input: string): string { + return Buffer.from(input).toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); +} + +export async function POST(request: NextRequest) { + try { + const appId = process.env.JITSI_APP_ID; + const appSecret = process.env.JITSI_APP_SECRET; + if (!appId || !appSecret) { + return NextResponse.json({ error: 'Jitsi is not configured' }, { status: 503 }); + } + + const cookieStore = await cookies(); + const sessionToken = cookieStore.get(sessionCookieName(0))?.value; + if (!sessionToken) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + const session = decryptSession(sessionToken); + if (!session) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + const email = session.username; + + const body = await request.json().catch(() => ({})); + const room = typeof body.room === 'string' ? body.room.trim() : ''; + if (!room || !/^[a-z0-9-]{1,100}$/i.test(room)) { + return NextResponse.json({ error: 'Invalid room name' }, { status: 400 }); + } + + const domain = new URL(JITSI_URL).hostname; + const now = Math.floor(Date.now() / 1000); + const header = { alg: 'HS256', typ: 'JWT' }; + const payload = { + iss: 'bulwark-webmail', + sub: domain, + aud: appId, + room, + iat: now, + exp: now + 86400, + context: { + user: { + email, + name: email.split('@')[0], + }, + }, + }; + + const signingInput = `${b64u(JSON.stringify(header))}.${b64u(JSON.stringify(payload))}`; + const signature = createHmac('sha256', appSecret).update(signingInput).digest(); + const token = `${signingInput}.${base64url(signature)}`; + + logger.info('Jitsi token issued', { room, email }); + + return NextResponse.json({ + token, + room, + url: `${JITSI_URL}/${encodeURIComponent(room)}`, + }); + } catch (error) { + const message = error instanceof Error ? error.message : 'Unknown error'; + logger.error('Jitsi token issuance failed', { error: message }); + return NextResponse.json({ error: message }, { status: 500 }); + } +} diff --git a/lib/admin/bundled-plugins.ts b/lib/admin/bundled-plugins.ts index 1204f63e..573f8173 100644 --- a/lib/admin/bundled-plugins.ts +++ b/lib/admin/bundled-plugins.ts @@ -75,6 +75,9 @@ const FIRST_PARTY_PLUGINS: FirstPartyPlugin[] = [ // webmail; this plugin adds a "User management" Settings entry that opens // the directory's user list. Force-enabled so it is always present. { id: 'manage-users', gate: 'manageUsersEnabled', forceEnable: true }, + // SRC video meetings (VNCtalk / Jitsi). "Start a meeting" asks the server + // for a signed JWT and opens the room in meet.src-advisory.com. + { id: 'jitsi-meet', gate: 'jitsiMeetEnabled', forceEnable: true }, ]; const ID_RE = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/; diff --git a/lib/admin/types.ts b/lib/admin/types.ts index 8aabdef8..a5f84664 100644 --- a/lib/admin/types.ts +++ b/lib/admin/types.ts @@ -56,6 +56,7 @@ export interface FeatureGates { calendarTasksEnabled: boolean; smimeEnabled: boolean; manageUsersEnabled: boolean; + jitsiMeetEnabled: boolean; externalContentEnabled: boolean; debugModeEnabled: boolean; folderIconsEnabled: boolean; @@ -92,6 +93,7 @@ export const DEFAULT_FEATURE_GATES: FeatureGates = { calendarTasksEnabled: true, smimeEnabled: true, manageUsersEnabled: true, + jitsiMeetEnabled: true, externalContentEnabled: true, debugModeEnabled: true, folderIconsEnabled: true, diff --git a/vnc/plugins/jitsi-meet/manifest.json b/vnc/plugins/jitsi-meet/manifest.json new file mode 100644 index 00000000..68a81360 --- /dev/null +++ b/vnc/plugins/jitsi-meet/manifest.json @@ -0,0 +1,14 @@ +{ + "id": "jitsi-meet", + "name": "Video meetings", + "version": "1.0.0", + "author": "SRC Advisory", + "description": "SRC video meetings (VNCtalk / Jitsi). Start a meeting from Settings: the plugin asks the server for a signed JWT and opens the room in meet.src-advisory.com.", + "type": "ui-extension", + "tier": "privileged", + "permissions": [ + "ui:settings-section" + ], + "entrypoint": "index.js", + "minAppVersion": "1.7.6" +} diff --git a/vnc/plugins/jitsi-meet/package-lock.json b/vnc/plugins/jitsi-meet/package-lock.json new file mode 100644 index 00000000..20c1477c --- /dev/null +++ b/vnc/plugins/jitsi-meet/package-lock.json @@ -0,0 +1,481 @@ +{ + "name": "bulwark-plugin-jitsi-meet", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "bulwark-plugin-jitsi-meet", + "version": "1.0.0", + "devDependencies": { + "esbuild": "^0.24.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz", + "integrity": "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.2.tgz", + "integrity": "sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.2.tgz", + "integrity": "sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.2.tgz", + "integrity": "sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.2.tgz", + "integrity": "sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.2.tgz", + "integrity": "sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.2.tgz", + "integrity": "sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.2.tgz", + "integrity": "sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.2.tgz", + "integrity": "sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.2.tgz", + "integrity": "sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.2.tgz", + "integrity": "sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.2.tgz", + "integrity": "sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.2.tgz", + "integrity": "sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.2.tgz", + "integrity": "sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.2.tgz", + "integrity": "sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.2.tgz", + "integrity": "sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.2.tgz", + "integrity": "sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.24.2.tgz", + "integrity": "sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.2.tgz", + "integrity": "sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.2.tgz", + "integrity": "sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.2.tgz", + "integrity": "sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.2.tgz", + "integrity": "sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.2.tgz", + "integrity": "sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.2.tgz", + "integrity": "sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.2.tgz", + "integrity": "sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/esbuild": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.2.tgz", + "integrity": "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.24.2", + "@esbuild/android-arm": "0.24.2", + "@esbuild/android-arm64": "0.24.2", + "@esbuild/android-x64": "0.24.2", + "@esbuild/darwin-arm64": "0.24.2", + "@esbuild/darwin-x64": "0.24.2", + "@esbuild/freebsd-arm64": "0.24.2", + "@esbuild/freebsd-x64": "0.24.2", + "@esbuild/linux-arm": "0.24.2", + "@esbuild/linux-arm64": "0.24.2", + "@esbuild/linux-ia32": "0.24.2", + "@esbuild/linux-loong64": "0.24.2", + "@esbuild/linux-mips64el": "0.24.2", + "@esbuild/linux-ppc64": "0.24.2", + "@esbuild/linux-riscv64": "0.24.2", + "@esbuild/linux-s390x": "0.24.2", + "@esbuild/linux-x64": "0.24.2", + "@esbuild/netbsd-arm64": "0.24.2", + "@esbuild/netbsd-x64": "0.24.2", + "@esbuild/openbsd-arm64": "0.24.2", + "@esbuild/openbsd-x64": "0.24.2", + "@esbuild/sunos-x64": "0.24.2", + "@esbuild/win32-arm64": "0.24.2", + "@esbuild/win32-ia32": "0.24.2", + "@esbuild/win32-x64": "0.24.2" + } + } + } +} diff --git a/vnc/plugins/jitsi-meet/package.json b/vnc/plugins/jitsi-meet/package.json new file mode 100644 index 00000000..62575337 --- /dev/null +++ b/vnc/plugins/jitsi-meet/package.json @@ -0,0 +1,14 @@ +{ + "name": "bulwark-plugin-jitsi-meet", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "build": "esbuild src/index.js --bundle --format=cjs --platform=browser --charset=utf8 --outfile=dist/index.js --external:react --external:react-dom --external:react-dom/client --external:react/jsx-runtime --external:@plugin-host", + "dev": "npm run build -- --watch", + "package": "npm run build && cp manifest.json dist/ && cd dist && zip -X ../jitsi-meet.zip manifest.json index.js" + }, + "devDependencies": { + "esbuild": "^0.24.0" + } +} diff --git a/vnc/plugins/jitsi-meet/src/index.js b/vnc/plugins/jitsi-meet/src/index.js new file mode 100644 index 00000000..1baa1f62 --- /dev/null +++ b/vnc/plugins/jitsi-meet/src/index.js @@ -0,0 +1,102 @@ +/** + * Video meetings — SRC Advisory webmail plugin (VNCtalk / Jitsi). + * + * Adds a "Video meeting" entry to Settings. "Start a meeting" generates a room + * name, asks the webmail server for a signed JWT (the server holds the shared + * JITSI_APP_SECRET — it never reaches the browser), then opens the room in + * meet.src-advisory.com with the token as a query param. + */ + +const host = require('@plugin-host'); +const React = require('react'); +const h = React.createElement; +const { useState } = React; + +const card = { + border: '1px solid var(--color-border, #e2e8f0)', + borderRadius: '8px', + padding: '16px', + background: 'var(--color-card, #fff)', + color: 'var(--color-foreground, #0f172a)', + maxWidth: '720px', +}; +const btnPrimary = { + font: 'inherit', + padding: '8px 14px', + borderRadius: '6px', + border: '1px solid var(--color-primary, #2563eb)', + background: 'var(--color-primary, #2563eb)', + color: '#fff', + cursor: 'pointer', +}; +const input = { + font: 'inherit', + padding: '6px 8px', + borderRadius: '6px', + border: '1px solid var(--color-input, #cbd5e1)', + background: 'var(--color-background, #fff)', + color: 'var(--color-foreground, #0f172a)', + marginRight: '8px', +}; + +function generateRoomName() { + const suffix = Math.random().toString(36).slice(2, 10); + return `meeting-${suffix}`; +} + +function SettingsSection() { + const [busy, setBusy] = useState(false); + const [joinRoom, setJoinRoom] = useState(''); + + async function openRoom(room) { + setBusy(true); + try { + const resp = await fetch('/api/jitsi/token', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ room }), + }); + const data = await resp.json(); + if (!resp.ok || !data.token) { + throw new Error(data.error || 'could not obtain meeting token'); + } + const url = `${data.url}?jwt=${encodeURIComponent(data.token)}`; + try { host.ui.openExternalUrl(url); } + catch { window.open(url, '_blank', 'noopener,noreferrer'); } + } catch (err) { + const msg = err && err.message ? err.message : String(err); + try { host.toast.error(`Could not start meeting: ${msg}`); } catch { /* ignore */ } + } finally { + setBusy(false); + } + } + + return h('div', { style: card }, + h('div', { style: { fontWeight: 600, marginBottom: '6px' } }, 'Video meeting'), + h('div', { style: { fontSize: '13px', lineHeight: 1.5, marginBottom: '14px', color: 'var(--color-muted-foreground, #64748b)' } }, + 'Start or join a SRC video meeting. The room is opened in meet.src-advisory.com.'), + h('div', { style: { display: 'flex', alignItems: 'center', marginBottom: '10px' } }, + h('button', { type: 'button', style: btnPrimary, disabled: busy, onClick: () => openRoom(generateRoomName()) }, + busy ? 'Starting…' : 'Start a meeting →'), + ), + h('div', { style: { display: 'flex', alignItems: 'center' } }, + h('input', { + type: 'text', + style: input, + placeholder: 'Room name', + value: joinRoom, + onChange: (e) => setJoinRoom(e.target.value), + }), + h('button', { type: 'button', style: btnPrimary, disabled: busy || !joinRoom.trim(), onClick: () => openRoom(joinRoom.trim()) }, + 'Join'), + ), + ); +} + +export const slots = { + 'settings-section': { component: SettingsSection, order: 90 }, +}; + +export async function activate(api) { + api.log.info('jitsi-meet plugin activated'); +}