Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
88 changes: 37 additions & 51 deletions apps/web/app/api/livekit/route.ts
Original file line number Diff line number Diff line change
@@ -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);
Comment thread
NdekoCode marked this conversation as resolved.

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) {

Check warning on line 10 in apps/web/app/api/livekit/route.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer using an optional chain expression instead, as it's more concise and easier to read.

See more on https://sonarcloud.io/project/issues?id=ever-co_ever-teams&issues=AaAL0qWN63KyUSPF2KhI&open=AaAL0qWN63KyUSPF2KhI&pullRequest=4424
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Comment thread
NdekoCode marked this conversation as resolved.
}

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' });
Comment thread
NdekoCode marked this conversation as resolved.
at.addGrant({
// Rooms are shared by link, so scoping per tenant keeps a leaked link within its tenant
room: `${user.tenantId}:${room}`,
Comment thread
NdekoCode marked this conversation as resolved.
roomJoin: true,
canPublish: true,
canSubscribe: true,
canPublishData: true
});
const token = await at.toJwt();
return NextResponse.json({ token: token });
Comment thread
NdekoCode marked this conversation as resolved.
Comment thread
NdekoCode marked this conversation as resolved.
} catch (error) {
console.error('Failed to generate token:', error);
return NextResponse.json({ error: 'Failed to generate token' }, { status: 500 });
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,7 @@ function LiveKitPage() {
}, [params]);

const { token } = useTokenLiveKit({
roomName: roomName || '',
username: user?.email || ''
roomName: roomName || ''
});

return (
Expand Down
24 changes: 11 additions & 13 deletions apps/web/core/hooks/common/use-live-kit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string | null>(() => {
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 };
}
7 changes: 3 additions & 4 deletions apps/web/core/services/server/livekitroom.ts
Original file line number Diff line number Diff line change
@@ -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);
Comment thread
NdekoCode marked this conversation as resolved.
Expand Down
Loading