ep_media_upload 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +4 -2
  2. package/index.js +41 -9
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -27,8 +27,9 @@ Etherpad plugin for secure file uploads via S3 presigned URLs.
27
27
  "expires": 900,
28
28
  "downloadExpires": 300
29
29
  },
30
- "fileTypes": ["pdf", "doc", "docx", "xls", "xlsx", "ppt", "pptx", "txt", "zip"],
31
- "maxFileSize": 52428800
30
+ "fileTypes": ["pdf", "doc", "docx", "xls", "xlsx", "ppt", "pptx", "txt", "zip", "mp3", "mp4", "wav", "mov"],
31
+ "maxFileSize": 52428800,
32
+ "inlineExtensions": ["mp3", "mp4", "wav", "mov", "webm", "ogg"]
32
33
  }
33
34
  ```
34
35
 
@@ -49,6 +50,7 @@ Etherpad plugin for secure file uploads via S3 presigned URLs.
49
50
  |--------|----------|---------|-------------|
50
51
  | `fileTypes` | No | all | Array of allowed extensions (without dots) |
51
52
  | `maxFileSize` | No | unlimited | Max file size in bytes |
53
+ | `inlineExtensions` | No | `[]` | Extensions to open inline in browser (streaming). Files not in this list will download. |
52
54
 
53
55
  ### Environment Variables
54
56
 
package/index.js CHANGED
@@ -133,7 +133,7 @@ const EXTENSION_MIME_MAP = {
133
133
 
134
134
  // Audio
135
135
  mp3: ['audio/mpeg', 'audio/mp3'],
136
- wav: ['audio/wav', 'audio/wave', 'audio/x-wav'],
136
+ wav: ['audio/wav', 'audio/wave', 'audio/x-wav', 'audio/vnd.wave'],
137
137
  ogg: ['audio/ogg'],
138
138
  m4a: ['audio/mp4', 'audio/x-m4a'],
139
139
  flac: ['audio/flac'],
@@ -281,6 +281,10 @@ exports.expressCreateServer = (hookName, context) => {
281
281
  return res.status(500).json({ error: 'Security module unavailable' });
282
282
  }
283
283
 
284
+ // Get client IP for rate limiting and audit logging
285
+ const clientIp = req.ip || req.headers['x-forwarded-for'] || req.connection?.remoteAddress || 'unknown';
286
+
287
+ let authorId = 'unknown';
284
288
  try {
285
289
  const sessionCookie = req.cookies?.sessionID || null;
286
290
  const token = req.cookies?.token || null;
@@ -288,16 +292,18 @@ exports.expressCreateServer = (hookName, context) => {
288
292
 
289
293
  const accessResult = await securityManager.checkAccess(padId, sessionCookie, token, user);
290
294
  if (accessResult.accessStatus !== 'grant') {
295
+ logger.warn(`[ep_media_upload] UPLOAD_DENIED: ip="${clientIp}" pad="${padId}" reason="access_denied"`);
291
296
  return res.status(403).json({ error: 'Access denied to this pad' });
292
297
  }
298
+ authorId = accessResult.authorID || 'unknown';
293
299
  } catch (authErr) {
294
300
  logger.error('[ep_media_upload] Access check error:', authErr);
295
301
  return res.status(500).json({ error: 'Access verification failed' });
296
302
  }
297
303
 
298
304
  /* ------------------ Rate limiting --------------------- */
299
- const ip = req.ip || req.headers['x-forwarded-for'] || req.connection?.remoteAddress || 'unknown';
300
- if (!_rateLimitCheck(ip)) {
305
+ if (!_rateLimitCheck(clientIp)) {
306
+ logger.warn(`[ep_media_upload] UPLOAD_RATE_LIMITED: ip="${clientIp}" pad="${padId}"`);
301
307
  return res.status(429).json({ error: 'Too many presign requests' });
302
308
  }
303
309
 
@@ -373,8 +379,8 @@ exports.expressCreateServer = (hookName, context) => {
373
379
 
374
380
  // Log upload request for audit trail
375
381
  // Note: Never log tokens or session cookies - only non-sensitive identifiers
376
- const userId = req.session?.user?.username || req.session?.authorId || 'anonymous';
377
- logger.info(`[ep_media_upload] UPLOAD: user="${userId}" pad="${padId}" file="${originalFilename}" s3key="${key}"`);
382
+ const username = req.session?.user?.username || 'anonymous';
383
+ logger.info(`[ep_media_upload] UPLOAD: author="${authorId}" user="${username}" ip="${clientIp}" pad="${padId}" file="${originalFilename}" s3key="${key}"`);
378
384
 
379
385
  // Return downloadUrl for hyperlink insertion (authenticated download endpoint)
380
386
  // Also return signedUrl for the actual S3 upload and contentDisposition for PUT headers
@@ -414,6 +420,10 @@ exports.expressCreateServer = (hookName, context) => {
414
420
  return res.status(500).json({ error: 'Security module unavailable' });
415
421
  }
416
422
 
423
+ // Get client IP for rate limiting and audit logging
424
+ const clientIp = req.ip || req.headers['x-forwarded-for'] || req.connection?.remoteAddress || 'unknown';
425
+
426
+ let authorId = 'unknown';
417
427
  try {
418
428
  const sessionCookie = req.cookies?.sessionID || null;
419
429
  const token = req.cookies?.token || null;
@@ -421,16 +431,18 @@ exports.expressCreateServer = (hookName, context) => {
421
431
 
422
432
  const accessResult = await securityManager.checkAccess(padId, sessionCookie, token, user);
423
433
  if (accessResult.accessStatus !== 'grant') {
434
+ logger.warn(`[ep_media_upload] DOWNLOAD_DENIED: ip="${clientIp}" pad="${padId}" file="${fileId}" reason="access_denied"`);
424
435
  return res.status(403).json({ error: 'Access denied to this pad' });
425
436
  }
437
+ authorId = accessResult.authorID || 'unknown';
426
438
  } catch (authErr) {
427
439
  logger.error('[ep_media_upload] Download access check error:', authErr);
428
440
  return res.status(500).json({ error: 'Access verification failed' });
429
441
  }
430
442
 
431
443
  /* ------------------ Rate limiting --------------------- */
432
- const ip = req.ip || req.headers['x-forwarded-for'] || req.connection?.remoteAddress || 'unknown';
433
- if (!_rateLimitCheck(ip)) {
444
+ if (!_rateLimitCheck(clientIp)) {
445
+ logger.warn(`[ep_media_upload] DOWNLOAD_RATE_LIMITED: ip="${clientIp}" pad="${padId}" file="${fileId}"`);
434
446
  return res.status(429).json({ error: 'Too many download requests' });
435
447
  }
436
448
 
@@ -455,11 +467,30 @@ exports.expressCreateServer = (hookName, context) => {
455
467
  const prefix = keyPrefix || '';
456
468
  const key = `${prefix}${padId}/${fileId}`;
457
469
 
470
+ // Extract file extension to determine inline vs attachment disposition
471
+ const fileExtension = getValidExtension(fileId);
472
+
473
+ // Get inlineExtensions from config (extensions that should open in browser)
474
+ // Default behavior is download (attachment) for all files
475
+ const inlineExtensions = settings.ep_media_upload?.inlineExtensions || [];
476
+ const shouldOpenInline = fileExtension &&
477
+ Array.isArray(inlineExtensions) &&
478
+ inlineExtensions.map(e => e.toLowerCase()).includes(fileExtension.toLowerCase());
479
+
480
+ // Determine Content-Disposition based on extension config
481
+ // Extract filename for Content-Disposition header (UUID.ext -> use as filename)
482
+ const filename = fileId.replace(/[^\w\-_.]/g, '_'); // Sanitize for header
483
+ const disposition = shouldOpenInline
484
+ ? `inline; filename="${filename}"`
485
+ : `attachment; filename="${filename}"`;
486
+
458
487
  // Generate presigned GET URL with short expiry
488
+ // Use ResponseContentDisposition to override the stored header
459
489
  const s3Client = new S3Client({ region });
460
490
  const getCommand = new GetObjectCommand({
461
491
  Bucket: bucket,
462
492
  Key: key,
493
+ ResponseContentDisposition: disposition,
463
494
  });
464
495
 
465
496
  // Use downloadExpires from config, default to 300 seconds (5 minutes)
@@ -467,8 +498,9 @@ exports.expressCreateServer = (hookName, context) => {
467
498
  const presignedGetUrl = await getSignedUrl(s3Client, getCommand, { expiresIn });
468
499
 
469
500
  // Log download request for audit trail
470
- const userId = req.session?.user?.username || req.session?.authorId || 'anonymous';
471
- logger.info(`[ep_media_upload] DOWNLOAD: user="${userId}" pad="${padId}" file="${fileId}"`);
501
+ const username = req.session?.user?.username || 'anonymous';
502
+ const dispositionType = shouldOpenInline ? 'inline' : 'attachment';
503
+ logger.info(`[ep_media_upload] DOWNLOAD: author="${authorId}" user="${username}" ip="${clientIp}" pad="${padId}" file="${fileId}" disposition="${dispositionType}"`);
472
504
 
473
505
  // Redirect to the presigned URL
474
506
  return res.redirect(302, presignedGetUrl);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "ep_media_upload",
3
3
  "description": "beta - Upload files to S3 and insert hyperlinks into the pad. Requires ep_hyperlinked_text.",
4
- "version": "0.2.0",
4
+ "version": "0.2.2",
5
5
  "author": {
6
6
  "name": "DCastelone",
7
7
  "url": "https://github.com/dcastelone"