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_SECRET = process.env.BBB_SECRET || crypto.randomBytes(32).toString('hex');
|
||||||
const PRESENTATION_TOKEN_TTL = 60 * 60 * 1000; // 1 hour
|
const PRESENTATION_TOKEN_TTL = 60 * 60 * 1000; // 1 hour
|
||||||
|
|
||||||
function signPresentationUrl(filename) {
|
function signPresentationUrl(roomUid, filename) {
|
||||||
const expires = Date.now() + PRESENTATION_TOKEN_TTL;
|
const expires = Date.now() + PRESENTATION_TOKEN_TTL;
|
||||||
const token = crypto.createHmac('sha256', PRESENTATION_TOKEN_SECRET)
|
const token = crypto.createHmac('sha256', PRESENTATION_TOKEN_SECRET)
|
||||||
.update(`${filename}:${expires}`)
|
.update(`${roomUid}/${filename}:${expires}`)
|
||||||
.digest('hex');
|
.digest('hex');
|
||||||
return { token, expires };
|
return { token, expires };
|
||||||
}
|
}
|
||||||
@@ -497,8 +497,8 @@ router.post('/:uid/start', authenticateToken, async (req, res) => {
|
|||||||
const loginURL = `${baseUrl}/join/${room.uid}`;
|
const loginURL = `${baseUrl}/join/${room.uid}`;
|
||||||
let presentationUrl = null;
|
let presentationUrl = null;
|
||||||
if (room.presentation_file) {
|
if (room.presentation_file) {
|
||||||
const { token, expires } = signPresentationUrl(room.presentation_file);
|
const { token, expires } = signPresentationUrl(room.uid, room.presentation_file);
|
||||||
presentationUrl = `${baseUrl}/api/rooms/presentations/${token}/${expires}/${room.presentation_file}`;
|
presentationUrl = `${baseUrl}/api/rooms/presentations/${token}/${expires}/${room.uid}/${encodeURIComponent(room.presentation_file)}`;
|
||||||
}
|
}
|
||||||
const analyticsCallbackURL = room.learning_analytics
|
const analyticsCallbackURL = room.learning_analytics
|
||||||
? `${baseUrl}/api/analytics/callback/${room.uid}?token=${getAnalyticsToken(room.uid)}`
|
? `${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)
|
// 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 filename,
|
// Token and expires are path segments so the URL ends with the original filename,
|
||||||
// allowing BBB to detect the file type from the extension.
|
// allowing BBB to detect the file type from the extension.
|
||||||
router.get('/presentations/:token/:expires/:filename', (req, res) => {
|
router.get('/presentations/:token/:expires/:roomUid/:filename', (req, res) => {
|
||||||
const { token, expires, filename } = req.params;
|
const { token, expires, roomUid, filename } = req.params;
|
||||||
|
|
||||||
if (!token || !expires) {
|
if (!token || !expires) {
|
||||||
return res.status(401).json({ error: 'Missing token' });
|
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)
|
const expected = crypto.createHmac('sha256', PRESENTATION_TOKEN_SECRET)
|
||||||
.update(`${filename}:${expires}`)
|
.update(`${roomUid}/${filename}:${expires}`)
|
||||||
.digest('hex');
|
.digest('hex');
|
||||||
|
|
||||||
if (!crypto.timingSafeEqual(Buffer.from(token), Buffer.from(expected))) {
|
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
|
// S8: prevent path traversal
|
||||||
const filepath = path.resolve(presentationsDir, filename);
|
const roomDir = path.resolve(presentationsDir, roomUid);
|
||||||
if (!filepath.startsWith(presentationsDir + path.sep)) {
|
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' });
|
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)
|
// Preserve original filename (sent as X-Filename header)
|
||||||
const rawName = req.headers['x-filename'];
|
const rawName = req.headers['x-filename'];
|
||||||
const originalName = rawName
|
const filename = rawName
|
||||||
? decodeURIComponent(rawName).replace(/[^a-zA-Z0-9._\- ]/g, '_').slice(0, 200)
|
? decodeURIComponent(rawName).replace(/[^a-zA-Z0-9._\- ]/g, '_').slice(0, 200)
|
||||||
: `presentation.${ext}`;
|
: `presentation.${ext}`;
|
||||||
|
|
||||||
const filename = `${room.uid}_${Date.now()}.${ext}`;
|
// Each room gets its own folder: uploads/presentations/{roomUID}/
|
||||||
const filepath = path.join(presentationsDir, filename);
|
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
|
// Remove old presentation file if exists
|
||||||
if (room.presentation_file) {
|
if (room.presentation_file) {
|
||||||
// S8: defense-in-depth path traversal check
|
const oldPath = path.resolve(roomDir, room.presentation_file);
|
||||||
const oldPath = path.resolve(presentationsDir, room.presentation_file);
|
if (oldPath.startsWith(roomDir + path.sep) && fs.existsSync(oldPath)) fs.unlinkSync(oldPath);
|
||||||
if (oldPath.startsWith(presentationsDir + path.sep) && fs.existsSync(oldPath)) fs.unlinkSync(oldPath);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fs.writeFileSync(filepath, buffer);
|
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]);
|
const updated = await db.get('SELECT * FROM rooms WHERE uid = ?', [req.params.uid]);
|
||||||
res.json({ room: updated });
|
res.json({ room: updated });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -825,11 +832,14 @@ router.delete('/:uid/presentation', authenticateToken, async (req, res) => {
|
|||||||
|
|
||||||
if (room.presentation_file) {
|
if (room.presentation_file) {
|
||||||
// S8: defense-in-depth path traversal check
|
// S8: defense-in-depth path traversal check
|
||||||
const filepath = path.resolve(presentationsDir, room.presentation_file);
|
const roomDir = path.join(presentationsDir, room.uid);
|
||||||
if (filepath.startsWith(presentationsDir + path.sep) && fs.existsSync(filepath)) fs.unlinkSync(filepath);
|
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]);
|
const updated = await db.get('SELECT * FROM rooms WHERE uid = ?', [req.params.uid]);
|
||||||
res.json({ room: updated });
|
res.json({ room: updated });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -717,7 +717,7 @@ export default function RoomDetail() {
|
|||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<p className="text-xs text-th-text-s">{t('room.presentationCurrent')}</p>
|
<p className="text-xs text-th-text-s">{t('room.presentationCurrent')}</p>
|
||||||
<p className="text-sm text-th-text font-medium truncate">
|
<p className="text-sm text-th-text font-medium truncate">
|
||||||
{room.presentation_name || `presentation.${room.presentation_file?.split('.').pop()}`}
|
{room.presentation_file}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user