feat(analytics): enhance analytics functionality with token validation and data extraction
All checks were successful
Build & Push Docker Image / build (push) Successful in 6m29s
All checks were successful
Build & Push Docker Image / build (push) Successful in 6m29s
This commit is contained in:
@@ -191,4 +191,8 @@ export async function publishRecording(recordID, publish) {
|
||||
return apiCall('publishRecordings', { recordID, publish: publish ? 'true' : 'false' });
|
||||
}
|
||||
|
||||
export function getAnalyticsToken(uid) {
|
||||
return crypto.createHmac('sha256', BBB_SECRET).update('analytics_' + uid).digest('hex');
|
||||
}
|
||||
|
||||
export { getRoomPasswords };
|
||||
|
||||
@@ -1,13 +1,24 @@
|
||||
import { Router } from 'express';
|
||||
import crypto from 'crypto';
|
||||
import { getDb } from '../config/database.js';
|
||||
import { authenticateToken } from '../middleware/auth.js';
|
||||
import { log } from '../config/logger.js';
|
||||
import { getAnalyticsToken } from '../config/bbb.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
// POST /api/analytics/callback/:uid - BBB Learning Analytics callback (no auth, called by BBB)
|
||||
// POST /api/analytics/callback/:uid?token=... - BBB Learning Analytics callback (token-secured)
|
||||
router.post('/callback/:uid', async (req, res) => {
|
||||
try {
|
||||
const { token } = req.query;
|
||||
const expectedToken = getAnalyticsToken(req.params.uid);
|
||||
|
||||
// Constant-time comparison to prevent timing attacks
|
||||
if (!token || token.length !== expectedToken.length ||
|
||||
!crypto.timingSafeEqual(Buffer.from(token), Buffer.from(expectedToken))) {
|
||||
return res.status(403).json({ error: 'Invalid token' });
|
||||
}
|
||||
|
||||
const db = getDb();
|
||||
const room = await db.get('SELECT id, uid, learning_analytics FROM rooms WHERE uid = ?', [req.params.uid]);
|
||||
|
||||
@@ -24,9 +35,10 @@ router.post('/callback/:uid', async (req, res) => {
|
||||
return res.status(400).json({ error: 'Invalid analytics data' });
|
||||
}
|
||||
|
||||
// Extract meeting info from BBB analytics payload
|
||||
const meetingId = data.intId || data.extId || room.uid;
|
||||
const meetingName = data.name || room.uid;
|
||||
// Extract meeting info from BBB learning analytics payload
|
||||
// Format: { meeting_id, internal_meeting_id, data: { metadata: { meeting_name }, duration, attendees, ... } }
|
||||
const meetingId = data.internal_meeting_id || data.meeting_id || room.uid;
|
||||
const meetingName = data.data?.metadata?.meeting_name || data.meeting_id || room.uid;
|
||||
|
||||
// Upsert: update if same meeting already exists (BBB sends updates during the meeting)
|
||||
const existing = await db.get(
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
endMeeting,
|
||||
getMeetingInfo,
|
||||
isMeetingRunning,
|
||||
getAnalyticsToken,
|
||||
} from '../config/bbb.js';
|
||||
import {
|
||||
isFederationEnabled,
|
||||
@@ -484,7 +485,7 @@ router.post('/:uid/start', authenticateToken, async (req, res) => {
|
||||
? `${baseUrl}/uploads/presentations/${room.presentation_file}`
|
||||
: null;
|
||||
const analyticsCallbackURL = room.learning_analytics
|
||||
? `${baseUrl}/api/analytics/callback/${room.uid}`
|
||||
? `${baseUrl}/api/analytics/callback/${room.uid}?token=${getAnalyticsToken(room.uid)}`
|
||||
: null;
|
||||
await createMeeting(room, baseUrl, loginURL, presentationUrl, analyticsCallbackURL);
|
||||
const avatarURL = getUserAvatarURL(req, req.user);
|
||||
@@ -632,7 +633,7 @@ router.post('/:uid/guest-join', guestJoinLimiter, async (req, res) => {
|
||||
const baseUrl = getBaseUrl(req);
|
||||
const loginURL = `${baseUrl}/join/${room.uid}`;
|
||||
const analyticsCallbackURL = room.learning_analytics
|
||||
? `${baseUrl}/api/analytics/callback/${room.uid}`
|
||||
? `${baseUrl}/api/analytics/callback/${room.uid}?token=${getAnalyticsToken(room.uid)}`
|
||||
: null;
|
||||
await createMeeting(room, baseUrl, loginURL, null, analyticsCallbackURL);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user