nodebb-plugin-pdf-secure2 1.4.2 → 1.5.0

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.
@@ -5,7 +5,12 @@
5
5
  "WebFetch(domain:github.com)",
6
6
  "Bash(dir:*)",
7
7
  "Bash(npm pack:*)",
8
- "Bash(tar:*)"
8
+ "Bash(tar:*)",
9
+ "Bash(grep:*)",
10
+ "Bash(find /c/Users/kadir/OneDrive/Masaüstü/Projeler/nodebb-plugin-pdf-secure/nodebb-plugin-pdf-secure -type f \\\\\\(-name *.conf -o -name *.config -o -name *nginx* -o -name *apache* \\\\\\))",
11
+ "Bash(xargs ls:*)",
12
+ "WebSearch",
13
+ "WebFetch(domain:community.nodebb.org)"
9
14
  ]
10
15
  }
11
16
  }
@@ -7,6 +7,7 @@ const pdfHandler = require('./pdf-handler');
7
7
  const geminiChat = require('./gemini-chat');
8
8
  const groups = require.main.require('./src/groups');
9
9
  const db = require.main.require('./src/database');
10
+ const topicAccess = require('./topic-access');
10
11
 
11
12
  const Controllers = module.exports;
12
13
 
@@ -60,8 +61,8 @@ async function checkRateLimit(uid, tier) {
60
61
  await db.sortedSetAdd(key, now, `${now}:${crypto.randomBytes(4).toString('hex')}`);
61
62
  return { allowed: true, used: count + 1, max: rateConfig.max };
62
63
  } catch (err) {
63
- // Graceful degradation: allow request if DB fails
64
- return { allowed: true, used: 0, max: rateConfig.max };
64
+ console.error('[PDF-Secure] Rate limit DB error:', err.message);
65
+ return { allowed: false, used: 0, max: rateConfig.max };
65
66
  }
66
67
  }
67
68
 
@@ -106,7 +107,8 @@ async function checkQuota(uid, tier) {
106
107
  }
107
108
  return { allowed: true, used: totalTokens, max: cfg.max, resetsIn };
108
109
  } catch (err) {
109
- return { allowed: true, used: 0, max: cfg.max, resetsIn: 0 };
110
+ console.error('[PDF-Secure] Quota check DB error:', err.message);
111
+ return { allowed: false, used: 0, max: cfg.max, resetsIn: 0 };
110
112
  }
111
113
  }
112
114
 
@@ -265,7 +267,8 @@ Controllers.handleChat = async function (req, res) {
265
267
  }
266
268
 
267
269
  // Body validation
268
- const { filename, question, history } = req.body;
270
+ const { filename, question, history, tid, detailMode } = req.body;
271
+ const useDetailMode = tier === 'vip' && detailMode === true;
269
272
 
270
273
  if (!filename || typeof filename !== 'string') {
271
274
  return res.status(400).json({ error: 'Missing or invalid filename' });
@@ -275,6 +278,12 @@ Controllers.handleChat = async function (req, res) {
275
278
  return res.status(400).json({ error: 'Invalid filename' });
276
279
  }
277
280
 
281
+ // Topic-level access control
282
+ const accessResult = await topicAccess.validate(req.uid, tid, safeName);
283
+ if (!accessResult.allowed) {
284
+ return res.status(403).json({ error: accessResult.reason || 'Access denied' });
285
+ }
286
+
278
287
  const trimmedQuestion = typeof question === 'string' ? question.trim() : '';
279
288
  if (!trimmedQuestion || trimmedQuestion.length > 2000) {
280
289
  return res.status(400).json({ error: 'Question is required (max 2000 characters)' });
@@ -312,7 +321,7 @@ Controllers.handleChat = async function (req, res) {
312
321
  }
313
322
 
314
323
  try {
315
- const result = await geminiChat.chat(safeName, trimmedQuestion, history || [], tier);
324
+ const result = await geminiChat.chat(safeName, trimmedQuestion, history || [], tier, useDetailMode);
316
325
  // Record actual token usage after successful AI response
317
326
  // Sanitize: clamp to [0, 1000000] to prevent NaN/negative/absurd values
318
327
  const tokensUsed = Math.max(0, Math.min(parseInt(result.tokensUsed, 10) || 0, 1000000));
@@ -331,7 +340,10 @@ Controllers.handleChat = async function (req, res) {
331
340
  return res.status(404).json({ error: 'PDF bulunamadı.', quota });
332
341
  }
333
342
  if (err.message === 'PDF too large for AI chat') {
334
- return res.status(413).json({ error: 'Bu PDF çok büyük. AI chat desteklemiyor.', quota });
343
+ const sizeMsg = tier === 'premium'
344
+ ? 'Bu PDF, Premium limiti (20MB) aşıyor. VIP üyelikle 50MB\'a kadar PDF\'lerle sohbet edebilirsiniz.'
345
+ : 'Bu PDF çok büyük. AI chat için maksimum dosya boyutu 50MB.';
346
+ return res.status(413).json({ error: sizeMsg, quota, showUpgrade: tier === 'premium' });
335
347
  }
336
348
  if (err.status === 429 || err.message.includes('rate limit') || err.message.includes('quota')) {
337
349
  return res.status(429).json({ error: 'AI servisi şu an yoğun. Lütfen birkaç saniye sonra tekrar deneyin.', quota });
@@ -345,6 +357,13 @@ Controllers.handleChat = async function (req, res) {
345
357
 
346
358
  // AI-powered question suggestions for a PDF
347
359
  const suggestionsRateLimit = new Map(); // uid -> lastRequestTime
360
+ // Periodic cleanup of stale rate limit entries (every 5 minutes)
361
+ setInterval(() => {
362
+ const cutoff = Date.now() - 60000; // entries older than 60s are stale
363
+ for (const [uid, ts] of suggestionsRateLimit.entries()) {
364
+ if (ts < cutoff) suggestionsRateLimit.delete(uid);
365
+ }
366
+ }, 5 * 60 * 1000).unref();
348
367
  Controllers.getSuggestions = async function (req, res) {
349
368
  if (!req.uid) {
350
369
  return res.status(401).json({ error: 'Authentication required' });
@@ -375,7 +394,7 @@ Controllers.getSuggestions = async function (req, res) {
375
394
  }
376
395
  suggestionsRateLimit.set(req.uid, now);
377
396
 
378
- const { filename } = req.query;
397
+ const { filename, tid } = req.query;
379
398
  if (!filename || typeof filename !== 'string') {
380
399
  return res.status(400).json({ error: 'Missing filename' });
381
400
  }
@@ -384,8 +403,14 @@ Controllers.getSuggestions = async function (req, res) {
384
403
  return res.status(400).json({ error: 'Invalid filename' });
385
404
  }
386
405
 
406
+ // Topic-level access control
407
+ const accessResult = await topicAccess.validate(req.uid, tid, safeName);
408
+ if (!accessResult.allowed) {
409
+ return res.status(403).json({ error: accessResult.reason || 'Access denied' });
410
+ }
411
+
387
412
  try {
388
- const suggestions = await geminiChat.generateSuggestions(safeName);
413
+ const suggestions = await geminiChat.generateSuggestions(safeName, tier);
389
414
  const quotaUsage = await getQuotaUsage(req.uid, tier);
390
415
  return res.json({ suggestions, quota: quotaUsage });
391
416
  } catch (err) {