FitnessApp/lib/jitsi.ts

140 lines
3.5 KiB
TypeScript

import jwt from 'jsonwebtoken';
export interface JitsiUser {
id: string;
name: string;
email?: string;
avatar?: string;
role: 'INSTRUCTOR' | 'CLIENT' | 'SUPER_ADMIN';
}
export interface JitsiRoomOptions {
tenantSlug: string;
classSessionId: string;
timestamp?: number;
}
export interface JitsiTokenOptions {
roomName: string;
tenantSlug: string;
user: JitsiUser;
expiresInSeconds?: number;
}
const JITSI_DOMAIN = process.env.JITSI_DOMAIN || 'jitsi.my127001.ca';
const JITSI_APP_ID = process.env.JITSI_APP_ID || 'fitness-app';
const JITSI_PUBLIC_URL = process.env.JITSI_PUBLIC_URL || `https://${JITSI_DOMAIN}`;
/**
* 1. Generates deterministic, tenant-isolated Jitsi room name:
* Format: ${tenantSlug}_${classSessionId}_${timestamp}
*/
export function generateJitsiRoomName(
tenantSlug: string,
classSessionId: string,
timestamp: number = Date.now()
): string {
const cleanSlug = tenantSlug.toLowerCase().replace(/[^a-z0-9_-]/g, '-');
const cleanSessionId = classSessionId.replace(/[^a-z0-9_-]/g, '-');
return `${cleanSlug}_${cleanSessionId}_${timestamp}`;
}
/**
* 2. Generates a signed Jitsi JWT token for host (INSTRUCTOR) or participant (CLIENT)
*/
export function generateJitsiToken({
roomName,
tenantSlug,
user,
expiresInSeconds = 7200, // 2 hours
}: JitsiTokenOptions): string {
const secret = process.env.JITSI_APP_SECRET;
if (!secret) {
throw new Error('JITSI_APP_SECRET is required to sign classroom access tokens');
}
const isHost = user.role === 'INSTRUCTOR' || user.role === 'SUPER_ADMIN';
const nowInSeconds = Math.floor(Date.now() / 1000);
const payload = {
aud: 'jitsi',
iss: JITSI_APP_ID,
sub: JITSI_DOMAIN,
room: roomName,
iat: nowInSeconds,
nbf: nowInSeconds - 5,
exp: nowInSeconds + expiresInSeconds,
context: {
user: {
id: user.id,
name: user.name,
email: user.email || `${user.id}@${tenantSlug}.local`,
avatar: user.avatar,
moderator: isHost,
},
features: {
recording: isHost,
livestreaming: isHost,
screenSharing: true,
lobby: isHost,
},
tenant: tenantSlug,
},
};
return jwt.sign(payload, secret, {
algorithm: 'HS256',
header: {
typ: 'JWT',
alg: 'HS256',
},
});
}
/**
* 3. Constructs the meeting room URL with unbranded options
*/
export function generateJitsiMeetingUrl(roomName: string, token?: string): string {
const base = `${JITSI_PUBLIC_URL}/${encodeURIComponent(roomName)}`;
if (!token) return base;
return `${base}?jwt=${encodeURIComponent(token)}`;
}
/**
* Pre-set feature flags and config overrides for unbranded classrooms
*/
export const DEFAULT_JITSI_CONFIG_OVERRIDES = {
disableThirdPartyRequests: true,
defaultLanguage: 'en',
hideLobbyButton: false,
toolbarButtons: [
'microphone',
'camera',
'desktop',
'chat',
'raisehand',
'tileview',
'hangup',
],
disableDeepLinking: true,
prejoinPageEnabled: false,
startWithAudioMuted: false,
startWithVideoMuted: false,
enableWelcomePage: false,
enableClosePage: false,
disableInviteFunctions: true,
};
export const DEFAULT_JITSI_INTERFACE_OVERRIDES = {
SHOW_JITSI_WATERMARK: false,
SHOW_WATERMARK_FOR_GUESTS: false,
SHOW_BRAND_WATERMARK: false,
DEFAULT_BACKGROUND: '#121212',
SHOW_PROMOTIONAL_CLOSE_PAGE: false,
MOBILE_APP_PROMO: false,
HIDE_DEEP_LINKING_LOGO: true,
TOOLBAR_ALWAYS_VISIBLE: false,
};
export { JITSI_DOMAIN, JITSI_PUBLIC_URL, JITSI_APP_ID };