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.
- package/.claude/settings.local.json +6 -1
- package/lib/controllers.js +33 -8
- package/lib/gemini-chat.js +452 -409
- package/lib/nonce-store.js +4 -4
- package/lib/pdf-handler.js +0 -1
- package/lib/topic-access.js +96 -0
- package/library.js +65 -5
- package/package.json +1 -1
- package/plugin.json +4 -0
- package/static/lib/main.js +3 -74
- package/static/templates/admin/plugins/pdf-secure.tpl +21 -0
- package/static/viewer-app.js +18 -62
- package/static/viewer.html +696 -70
|
@@ -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
|
}
|
package/lib/controllers.js
CHANGED
|
@@ -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
|
-
|
|
64
|
-
return { allowed:
|
|
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
|
-
|
|
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
|
-
|
|
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) {
|