fix: update presentation display to show filename instead of name
All checks were successful
Build & Push Docker Image / build (push) Successful in 4m21s
All checks were successful
Build & Push Docker Image / build (push) Successful in 4m21s
This commit is contained in:
@@ -40,10 +40,10 @@ if (!fs.existsSync(presentationsDir)) fs.mkdirSync(presentationsDir, { recursive
|
||||
const PRESENTATION_TOKEN_SECRET = process.env.BBB_SECRET || crypto.randomBytes(32).toString('hex');
|
||||
const PRESENTATION_TOKEN_TTL = 60 * 60 * 1000; // 1 hour
|
||||
|
||||
function signPresentationUrl(filename) {
|
||||
function signPresentationUrl(roomUid, filename) {
|
||||
const expires = Date.now() + PRESENTATION_TOKEN_TTL;
|
||||
const token = crypto.createHmac('sha256', PRESENTATION_TOKEN_SECRET)
|
||||
.update(`${filename}:${expires}`)
|
||||
.update(`${roomUid}/${filename}:${expires}`)
|
||||
.digest('hex');
|
||||
return { token, expires };
|
||||
}
|
||||
@@ -497,8 +497,8 @@ router.post('/:uid/start', authenticateToken, async (req, res) => {
|
||||
const loginURL = `${baseUrl}/join/${room.uid}`;
|
||||
let presentationUrl = null;
|
||||
if (room.presentation_file) {
|
||||
const { token, expires } = signPresentationUrl(room.presentation_file);
|
||||
presentationUrl = `${baseUrl}/api/rooms/presentations/${token}/${expires}/${room.presentation_file}`;
|
||||
const { token, expires } = signPresentationUrl(room.uid, room.presentation_file);
|
||||
presentationUrl = `${baseUrl}/api/rooms/presentations/${token}/${expires}/${room.uid}/${encodeURIComponent(room.presentation_file)}`;
|
||||
}
|
||||
const analyticsCallbackURL = room.learning_analytics
|
||||
? `${baseUrl}/api/analytics/callback/${room.uid}?token=${getAnalyticsToken(room.uid)}`
|
||||
@@ -702,11 +702,11 @@ router.get('/:uid/status', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/rooms/presentations/:token/:expires/:filename - Serve presentation file (token-protected for BBB)
|
||||
// Token and expires are path segments so the URL ends with the filename,
|
||||
// GET /api/rooms/presentations/:token/:expires/:roomUid/:filename - Serve presentation file (token-protected for BBB)
|
||||
// Token and expires are path segments so the URL ends with the original filename,
|
||||
// allowing BBB to detect the file type from the extension.
|
||||
router.get('/presentations/:token/:expires/:filename', (req, res) => {
|
||||
const { token, expires, filename } = req.params;
|
||||
router.get('/presentations/:token/:expires/:roomUid/:filename', (req, res) => {
|
||||
const { token, expires, roomUid, filename } = req.params;
|
||||
|
||||
if (!token || !expires) {
|
||||
return res.status(401).json({ error: 'Missing token' });
|
||||
@@ -718,7 +718,7 @@ router.get('/presentations/:token/:expires/:filename', (req, res) => {
|
||||
}
|
||||
|
||||
const expected = crypto.createHmac('sha256', PRESENTATION_TOKEN_SECRET)
|
||||
.update(`${filename}:${expires}`)
|
||||
.update(`${roomUid}/${filename}:${expires}`)
|
||||
.digest('hex');
|
||||
|
||||
if (!crypto.timingSafeEqual(Buffer.from(token), Buffer.from(expected))) {
|
||||
@@ -726,8 +726,9 @@ router.get('/presentations/:token/:expires/:filename', (req, res) => {
|
||||
}
|
||||
|
||||
// S8: prevent path traversal
|
||||
const filepath = path.resolve(presentationsDir, filename);
|
||||
if (!filepath.startsWith(presentationsDir + path.sep)) {
|
||||
const roomDir = path.resolve(presentationsDir, roomUid);
|
||||
const filepath = path.resolve(roomDir, filename);
|
||||
if (!filepath.startsWith(presentationsDir + path.sep) || !filepath.startsWith(roomDir + path.sep)) {
|
||||
return res.status(400).json({ error: 'Invalid filename' });
|
||||
}
|
||||
|
||||
@@ -792,22 +793,28 @@ router.post('/:uid/presentation', authenticateToken, async (req, res) => {
|
||||
|
||||
// Preserve original filename (sent as X-Filename header)
|
||||
const rawName = req.headers['x-filename'];
|
||||
const originalName = rawName
|
||||
const filename = rawName
|
||||
? decodeURIComponent(rawName).replace(/[^a-zA-Z0-9._\- ]/g, '_').slice(0, 200)
|
||||
: `presentation.${ext}`;
|
||||
|
||||
const filename = `${room.uid}_${Date.now()}.${ext}`;
|
||||
const filepath = path.join(presentationsDir, filename);
|
||||
// Each room gets its own folder: uploads/presentations/{roomUID}/
|
||||
const roomDir = path.join(presentationsDir, room.uid);
|
||||
if (!fs.existsSync(roomDir)) fs.mkdirSync(roomDir, { recursive: true });
|
||||
const filepath = path.join(roomDir, filename);
|
||||
|
||||
// S8: defense-in-depth path traversal check
|
||||
if (!path.resolve(filepath).startsWith(roomDir + path.sep)) {
|
||||
return res.status(400).json({ error: 'Invalid filename' });
|
||||
}
|
||||
|
||||
// Remove old presentation file if exists
|
||||
if (room.presentation_file) {
|
||||
// S8: defense-in-depth path traversal check
|
||||
const oldPath = path.resolve(presentationsDir, room.presentation_file);
|
||||
if (oldPath.startsWith(presentationsDir + path.sep) && fs.existsSync(oldPath)) fs.unlinkSync(oldPath);
|
||||
const oldPath = path.resolve(roomDir, room.presentation_file);
|
||||
if (oldPath.startsWith(roomDir + path.sep) && fs.existsSync(oldPath)) fs.unlinkSync(oldPath);
|
||||
}
|
||||
|
||||
fs.writeFileSync(filepath, buffer);
|
||||
await db.run('UPDATE rooms SET presentation_file = ?, presentation_name = ?, updated_at = CURRENT_TIMESTAMP WHERE uid = ?', [filename, originalName, req.params.uid]);
|
||||
await db.run('UPDATE rooms SET presentation_file = ?, updated_at = CURRENT_TIMESTAMP WHERE uid = ?', [filename, req.params.uid]);
|
||||
const updated = await db.get('SELECT * FROM rooms WHERE uid = ?', [req.params.uid]);
|
||||
res.json({ room: updated });
|
||||
} catch (err) {
|
||||
@@ -825,11 +832,14 @@ router.delete('/:uid/presentation', authenticateToken, async (req, res) => {
|
||||
|
||||
if (room.presentation_file) {
|
||||
// S8: defense-in-depth path traversal check
|
||||
const filepath = path.resolve(presentationsDir, room.presentation_file);
|
||||
if (filepath.startsWith(presentationsDir + path.sep) && fs.existsSync(filepath)) fs.unlinkSync(filepath);
|
||||
const roomDir = path.join(presentationsDir, room.uid);
|
||||
const filepath = path.resolve(roomDir, room.presentation_file);
|
||||
if (filepath.startsWith(roomDir + path.sep) && fs.existsSync(filepath)) fs.unlinkSync(filepath);
|
||||
// Remove empty room folder
|
||||
if (fs.existsSync(roomDir) && fs.readdirSync(roomDir).length === 0) fs.rmdirSync(roomDir);
|
||||
}
|
||||
|
||||
await db.run('UPDATE rooms SET presentation_file = NULL, presentation_name = NULL, updated_at = CURRENT_TIMESTAMP WHERE uid = ?', [req.params.uid]);
|
||||
await db.run('UPDATE rooms SET presentation_file = NULL, updated_at = CURRENT_TIMESTAMP WHERE uid = ?', [req.params.uid]);
|
||||
const updated = await db.get('SELECT * FROM rooms WHERE uid = ?', [req.params.uid]);
|
||||
res.json({ room: updated });
|
||||
} catch (err) {
|
||||
|
||||
Reference in New Issue
Block a user