diff --git a/.gitignore b/.gitignore index 4d0f7d18d1..fb8a798768 100644 --- a/.gitignore +++ b/.gitignore @@ -122,6 +122,8 @@ megalinter-reports/ /.deploy/redis/jitsu_users_recognition/data/*.rdb /.deploy/jitsu/server/data/logs/events +/graphify-out + .cursor\rules\nx-rules.mdc .github\instructions\nx.instructions.md diff --git a/apps/web/app/api/livekit/route.ts b/apps/web/app/api/livekit/route.ts index 8e07b29b99..1d535871a4 100644 --- a/apps/web/app/api/livekit/route.ts +++ b/apps/web/app/api/livekit/route.ts @@ -1,59 +1,45 @@ -import { AccessToken } from "livekit-server-sdk"; -import { NextRequest, NextResponse } from "next/server"; +import { authenticatedGuard } from '@/core/services/server/guards/authenticated-guard-app'; +import { AccessToken } from 'livekit-server-sdk'; +import { NextRequest, NextResponse } from 'next/server'; export async function GET(req: NextRequest) { - const room = req.nextUrl.searchParams.get("roomName"); - const username = req.nextUrl.searchParams.get("username"); + const res = new NextResponse(); + const { user } = await authenticatedGuard(req, res); - if (!room || typeof room !== 'string' || room.trim() === '') { - return NextResponse.json( - { error: 'Missing or invalid "roomName" query parameter' }, - { status: 400 } - ); - } + // Session tenant, not the guard's auth-tenant-id cookie: that one is client-writable + if (!user || !user.tenantId) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } - if (!username || typeof username !== 'string' || username.trim() === '') { - return NextResponse.json( - { error: 'Missing or invalid "username" query parameter' }, - { status: 400 } - ); - } + const room = req.nextUrl.searchParams.get('roomName')?.trim(); - const apiKey = process.env.LIVEKIT_API_KEY; - const apiSecret = process.env.LIVEKIT_API_SECRET; - const wsUrl = process.env.NEXT_PUBLIC_LIVEKIT_URL; + if (!room) { + return NextResponse.json({ error: 'Missing or invalid "roomName" query parameter' }, { status: 400 }); + } - if (!apiKey || !apiSecret || !wsUrl) { - console.error("Server misconfigured: missing environment variables."); - return NextResponse.json( - { error: "Server misconfigured" }, - { status: 500 } - ); - } + const apiKey = process.env.LIVEKIT_API_KEY; + const apiSecret = process.env.LIVEKIT_API_SECRET; + const wsUrl = process.env.NEXT_PUBLIC_LIVEKIT_URL; - try { - const at = new AccessToken(apiKey, apiSecret, { identity: username, ttl: '1h' }); - at.addGrant({ - room, - roomJoin: true, - canPublish: true, - canSubscribe: true, - roomRecord: true, - roomCreate: true, - roomAdmin: true, - recorder: true, - roomList: true, - canUpdateOwnMetadata: true, - agent: true, - canPublishData: true, - }); - const token = await at.toJwt(); - return NextResponse.json({ token: token }); - } catch (error) { - console.error("Failed to generate token:", error); - return NextResponse.json( - { error: "Failed to generate token" }, - { status: 500 } - ); - } + if (!apiKey || !apiSecret || !wsUrl) { + console.error('Server misconfigured: missing environment variables.'); + return NextResponse.json({ error: 'Server misconfigured' }, { status: 500 }); + } + + try { + const at = new AccessToken(apiKey, apiSecret, { identity: user.email || user.id, ttl: '1h' }); + at.addGrant({ + // Rooms are shared by link, so scoping per tenant keeps a leaked link within its tenant + room: `${user.tenantId}:${room}`, + roomJoin: true, + canPublish: true, + canSubscribe: true, + canPublishData: true + }); + const token = await at.toJwt(); + return NextResponse.json({ token: token }); + } catch (error) { + console.error('Failed to generate token:', error); + return NextResponse.json({ error: 'Failed to generate token' }, { status: 500 }); + } } diff --git a/apps/web/core/components/pages/meet/livekit/page-component.tsx b/apps/web/core/components/pages/meet/livekit/page-component.tsx index 6c700958c8..0f8e4e7aa3 100644 --- a/apps/web/core/components/pages/meet/livekit/page-component.tsx +++ b/apps/web/core/components/pages/meet/livekit/page-component.tsx @@ -30,8 +30,7 @@ function LiveKitPage() { }, [params]); const { token } = useTokenLiveKit({ - roomName: roomName || '', - username: user?.email || '' + roomName: roomName || '' }); return ( diff --git a/apps/web/core/hooks/common/use-live-kit.ts b/apps/web/core/hooks/common/use-live-kit.ts index eb0049b346..faf2e129c3 100644 --- a/apps/web/core/hooks/common/use-live-kit.ts +++ b/apps/web/core/hooks/common/use-live-kit.ts @@ -4,29 +4,27 @@ import { useEffect, useState } from 'react'; interface ITokenLiveKitProps { roomName: string; - username: string; } -export function useTokenLiveKit({ roomName, username }: ITokenLiveKitProps) { - const [token, setToken] = useState(() => { - if (typeof window !== 'undefined') { - return window.localStorage.getItem('token-live-kit'); - } - return null; - }); +export function useTokenLiveKit({ roomName }: ITokenLiveKitProps) { + const [issued, setIssued] = useState<{ room: string; token: string } | null>(null); useEffect(() => { + if (!roomName) return; + const fetchToken = async () => { try { - const response = await tokenLiveKitRoom({ roomName, username }); - window.localStorage.setItem('token-live-kit', response.token); - setToken(response.token); + const response = await tokenLiveKitRoom({ roomName }); + if (!response?.token) return; + setIssued({ room: roomName, token: response.token }); } catch (error) { console.error('Failed to fetch token:', error); } }; fetchToken(); - }, [roomName, username]); + }, [roomName]); - return { token }; + // A token only grants the room it was issued for, so handing back one from a previous + // room would publish local tracks into the room the user just left + return { token: issued?.room === roomName ? issued.token : null }; } diff --git a/apps/web/core/services/server/livekitroom.ts b/apps/web/core/services/server/livekitroom.ts index e388232f55..f65f1eb87d 100644 --- a/apps/web/core/services/server/livekitroom.ts +++ b/apps/web/core/services/server/livekitroom.ts @@ -1,10 +1,9 @@ import { ILiveKitCredentials } from '@/core/types/interfaces/integrations/livekit-credentials'; -export async function tokenLiveKitRoom({ roomName, username }: ILiveKitCredentials) { +export async function tokenLiveKitRoom({ roomName }: ILiveKitCredentials) { try { - const response = await fetch( - `/api/livekit?roomName=${roomName ?? 'default'}&username=${username ?? 'employee'}` - ); + const query = new URLSearchParams({ roomName: roomName ?? 'default' }); + const response = await fetch(`/api/livekit?${query.toString()}`); return await response.json(); } catch (e) { console.error(e);