nodebb-plugin-pdf-secure2 1.5.0 → 1.5.1
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/lib/controllers.js +130 -24
- package/lib/gemini-chat.js +33 -5
- package/library.js +11 -0
- package/package.json +1 -1
- package/static/lib/admin.js +25 -0
- package/static/templates/admin/plugins/pdf-secure.tpl +18 -2
package/lib/controllers.js
CHANGED
|
@@ -27,6 +27,12 @@ const QUOTAS = {
|
|
|
27
27
|
|
|
28
28
|
// Apply admin-configured quota settings (called from library.js on init)
|
|
29
29
|
Controllers.initQuotaSettings = function (settings) {
|
|
30
|
+
console.log('[PDF-Secure][DEBUG] initQuotaSettings called, settings:', JSON.stringify({
|
|
31
|
+
quotaPremiumTokens: settings?.quotaPremiumTokens,
|
|
32
|
+
quotaVipTokens: settings?.quotaVipTokens,
|
|
33
|
+
quotaWindowHours: settings?.quotaWindowHours,
|
|
34
|
+
geminiApiKey: settings?.geminiApiKey ? '***SET***' : '***EMPTY***',
|
|
35
|
+
}));
|
|
30
36
|
if (!settings) return;
|
|
31
37
|
const premiumTokens = parseInt(settings.quotaPremiumTokens, 10);
|
|
32
38
|
const vipTokens = parseInt(settings.quotaVipTokens, 10);
|
|
@@ -39,6 +45,7 @@ Controllers.initQuotaSettings = function (settings) {
|
|
|
39
45
|
QUOTAS.premium.window = windowMs;
|
|
40
46
|
QUOTAS.vip.window = windowMs;
|
|
41
47
|
}
|
|
48
|
+
console.log('[PDF-Secure][DEBUG] Final QUOTAS after init:', JSON.stringify(QUOTAS));
|
|
42
49
|
};
|
|
43
50
|
|
|
44
51
|
// DB-backed sliding window rate limiter
|
|
@@ -54,14 +61,16 @@ async function checkRateLimit(uid, tier) {
|
|
|
54
61
|
await db.sortedSetRemoveRangeByScore(key, '-inf', windowStart);
|
|
55
62
|
// Count remaining entries in window
|
|
56
63
|
const count = await db.sortedSetCard(key);
|
|
64
|
+
console.log('[PDF-Secure][DEBUG] checkRateLimit uid=%s tier=%s count=%d max=%d', uid, tier, count, rateConfig.max);
|
|
57
65
|
if (count >= rateConfig.max) {
|
|
66
|
+
console.log('[PDF-Secure][DEBUG] Rate limit BLOCKED uid=%s (%d >= %d)', uid, count, rateConfig.max);
|
|
58
67
|
return { allowed: false, used: count, max: rateConfig.max };
|
|
59
68
|
}
|
|
60
69
|
// Add current request (unique member via timestamp + random suffix)
|
|
61
70
|
await db.sortedSetAdd(key, now, `${now}:${crypto.randomBytes(4).toString('hex')}`);
|
|
62
71
|
return { allowed: true, used: count + 1, max: rateConfig.max };
|
|
63
72
|
} catch (err) {
|
|
64
|
-
console.error('[PDF-Secure] Rate limit DB
|
|
73
|
+
console.error('[PDF-Secure][DEBUG] Rate limit DB ERROR:', err.message, err.stack);
|
|
65
74
|
return { allowed: false, used: 0, max: rateConfig.max };
|
|
66
75
|
}
|
|
67
76
|
}
|
|
@@ -71,21 +80,29 @@ async function checkRateLimit(uid, tier) {
|
|
|
71
80
|
function sumTokensFromMembers(members) {
|
|
72
81
|
let total = 0;
|
|
73
82
|
for (const m of members) {
|
|
74
|
-
const
|
|
75
|
-
|
|
83
|
+
const raw = typeof m === 'string' ? m : m.value || '';
|
|
84
|
+
const parts = raw.split(':');
|
|
85
|
+
const tokenVal = parseInt(parts[2], 10) || 0;
|
|
86
|
+
total += tokenVal;
|
|
87
|
+
// Log each member for debugging (only if suspicious values)
|
|
88
|
+
if (tokenVal > 50000 || tokenVal < 0 || isNaN(parseInt(parts[2], 10))) {
|
|
89
|
+
console.log('[PDF-Secure][DEBUG] sumTokens UNUSUAL member: raw="%s" parts=%j tokenVal=%d', raw, parts, tokenVal);
|
|
90
|
+
}
|
|
76
91
|
}
|
|
77
92
|
return total;
|
|
78
93
|
}
|
|
79
94
|
|
|
80
95
|
// Token-based 4-hour sliding window quota (DB-backed sorted set)
|
|
81
96
|
// Read-only check — does NOT add entries (call recordQuotaUsage after AI response)
|
|
82
|
-
// Returns { allowed, used, max, resetsIn }
|
|
97
|
+
// Returns { allowed, used, max, resetsIn, dbError }
|
|
83
98
|
async function checkQuota(uid, tier) {
|
|
84
99
|
const cfg = QUOTAS[tier] || QUOTAS.premium;
|
|
85
100
|
const key = `pdf-secure:quota:${uid}`;
|
|
86
101
|
const now = Date.now();
|
|
87
102
|
const windowStart = now - cfg.window;
|
|
88
103
|
|
|
104
|
+
console.log('[PDF-Secure][DEBUG] checkQuota START uid=%s tier=%s key=%s max=%d window=%dms', uid, tier, key, cfg.max, cfg.window);
|
|
105
|
+
|
|
89
106
|
try {
|
|
90
107
|
// Remove expired entries outside the 4-hour window
|
|
91
108
|
await db.sortedSetRemoveRangeByScore(key, '-inf', windowStart);
|
|
@@ -93,22 +110,33 @@ async function checkQuota(uid, tier) {
|
|
|
93
110
|
const members = await db.getSortedSetRange(key, 0, -1);
|
|
94
111
|
const totalTokens = sumTokensFromMembers(members);
|
|
95
112
|
|
|
113
|
+
console.log('[PDF-Secure][DEBUG] checkQuota uid=%s members=%d totalTokens=%d max=%d', uid, members.length, totalTokens, cfg.max);
|
|
114
|
+
// Log first 5 members for debugging
|
|
115
|
+
if (members.length > 0) {
|
|
116
|
+
console.log('[PDF-Secure][DEBUG] checkQuota uid=%s sample members: %j', uid, members.slice(0, 5));
|
|
117
|
+
}
|
|
118
|
+
|
|
96
119
|
// Calculate resetsIn from oldest entry
|
|
97
120
|
let resetsIn = 0;
|
|
98
121
|
if (members.length > 0) {
|
|
99
122
|
const oldest = await db.getSortedSetRangeWithScores(key, 0, 0);
|
|
100
123
|
if (oldest && oldest.length > 0) {
|
|
101
124
|
resetsIn = Math.max(0, (oldest[0].score + cfg.window) - now);
|
|
125
|
+
console.log('[PDF-Secure][DEBUG] checkQuota uid=%s oldestScore=%d resetsIn=%dms (%s min)', uid, oldest[0].score, resetsIn, Math.round(resetsIn / 60000));
|
|
102
126
|
}
|
|
103
127
|
}
|
|
104
128
|
|
|
105
129
|
if (totalTokens >= cfg.max) {
|
|
130
|
+
console.log('[PDF-Secure][DEBUG] checkQuota BLOCKED uid=%s totalTokens=%d >= max=%d', uid, totalTokens, cfg.max);
|
|
106
131
|
return { allowed: false, used: totalTokens, max: cfg.max, resetsIn };
|
|
107
132
|
}
|
|
133
|
+
console.log('[PDF-Secure][DEBUG] checkQuota ALLOWED uid=%s totalTokens=%d < max=%d', uid, totalTokens, cfg.max);
|
|
108
134
|
return { allowed: true, used: totalTokens, max: cfg.max, resetsIn };
|
|
109
135
|
} catch (err) {
|
|
110
|
-
console.error('[PDF-Secure]
|
|
111
|
-
|
|
136
|
+
console.error('[PDF-Secure][DEBUG] checkQuota DB ERROR uid=%s:', uid, err.message, err.stack);
|
|
137
|
+
// Fail-open on DB error: allow the request but log the issue
|
|
138
|
+
// Previously this was fail-closed which caused "kota doldu" on any DB hiccup
|
|
139
|
+
return { allowed: true, used: 0, max: cfg.max, resetsIn: 0, dbError: true };
|
|
112
140
|
}
|
|
113
141
|
}
|
|
114
142
|
|
|
@@ -118,10 +146,13 @@ async function recordQuotaUsage(uid, tier, tokensUsed) {
|
|
|
118
146
|
const cfg = QUOTAS[tier] || QUOTAS.premium;
|
|
119
147
|
const key = `pdf-secure:quota:${uid}`;
|
|
120
148
|
const now = Date.now();
|
|
149
|
+
const member = `${now}:${crypto.randomBytes(4).toString('hex')}:${tokensUsed}`;
|
|
150
|
+
|
|
151
|
+
console.log('[PDF-Secure][DEBUG] recordQuotaUsage uid=%s tier=%s tokensUsed=%d member=%s', uid, tier, tokensUsed, member);
|
|
121
152
|
|
|
122
153
|
try {
|
|
123
154
|
// Add entry with token count encoded in member string
|
|
124
|
-
await db.sortedSetAdd(key, now,
|
|
155
|
+
await db.sortedSetAdd(key, now, member);
|
|
125
156
|
|
|
126
157
|
// Re-read total for accurate response
|
|
127
158
|
const members = await db.getSortedSetRange(key, 0, -1);
|
|
@@ -135,8 +166,10 @@ async function recordQuotaUsage(uid, tier, tokensUsed) {
|
|
|
135
166
|
}
|
|
136
167
|
}
|
|
137
168
|
|
|
169
|
+
console.log('[PDF-Secure][DEBUG] recordQuotaUsage uid=%s afterRecord: totalTokens=%d members=%d max=%d', uid, totalTokens, members.length, cfg.max);
|
|
138
170
|
return { used: totalTokens, max: cfg.max, resetsIn };
|
|
139
171
|
} catch (err) {
|
|
172
|
+
console.error('[PDF-Secure][DEBUG] recordQuotaUsage DB ERROR uid=%s:', uid, err.message, err.stack);
|
|
140
173
|
return { used: 0, max: cfg.max, resetsIn: 0 };
|
|
141
174
|
}
|
|
142
175
|
}
|
|
@@ -148,6 +181,8 @@ async function getQuotaUsage(uid, tier) {
|
|
|
148
181
|
const now = Date.now();
|
|
149
182
|
const windowStart = now - cfg.window;
|
|
150
183
|
|
|
184
|
+
console.log('[PDF-Secure][DEBUG] getQuotaUsage uid=%s tier=%s', uid, tier);
|
|
185
|
+
|
|
151
186
|
try {
|
|
152
187
|
await db.sortedSetRemoveRangeByScore(key, '-inf', windowStart);
|
|
153
188
|
const members = await db.getSortedSetRange(key, 0, -1);
|
|
@@ -159,8 +194,10 @@ async function getQuotaUsage(uid, tier) {
|
|
|
159
194
|
resetsIn = Math.max(0, (oldest[0].score + cfg.window) - now);
|
|
160
195
|
}
|
|
161
196
|
}
|
|
197
|
+
console.log('[PDF-Secure][DEBUG] getQuotaUsage uid=%s result: used=%d max=%d members=%d resetsIn=%dms', uid, totalTokens, cfg.max, members.length, resetsIn);
|
|
162
198
|
return { used: totalTokens, max: cfg.max, resetsIn };
|
|
163
199
|
} catch (err) {
|
|
200
|
+
console.error('[PDF-Secure][DEBUG] getQuotaUsage DB ERROR uid=%s:', uid, err.message, err.stack);
|
|
164
201
|
return { used: 0, max: cfg.max, resetsIn: 0 };
|
|
165
202
|
}
|
|
166
203
|
}
|
|
@@ -224,13 +261,17 @@ Controllers.servePdfBinary = async function (req, res) {
|
|
|
224
261
|
};
|
|
225
262
|
|
|
226
263
|
Controllers.handleChat = async function (req, res) {
|
|
264
|
+
console.log('[PDF-Secure][DEBUG] handleChat START uid=%s body.filename=%s', req.uid, req.body?.filename);
|
|
265
|
+
|
|
227
266
|
// Authentication gate
|
|
228
267
|
if (!req.uid) {
|
|
268
|
+
console.log('[PDF-Secure][DEBUG] handleChat REJECTED: not authenticated');
|
|
229
269
|
return res.status(401).json({ error: 'Authentication required' });
|
|
230
270
|
}
|
|
231
271
|
|
|
232
272
|
// Check AI availability
|
|
233
273
|
if (!geminiChat.isAvailable()) {
|
|
274
|
+
console.log('[PDF-Secure][DEBUG] handleChat REJECTED: AI not available (no API key?)');
|
|
234
275
|
return res.status(503).json({ error: 'AI chat is not configured' });
|
|
235
276
|
}
|
|
236
277
|
|
|
@@ -242,28 +283,42 @@ Controllers.handleChat = async function (req, res) {
|
|
|
242
283
|
groups.isMember(req.uid, 'VIP'),
|
|
243
284
|
]);
|
|
244
285
|
const isPremium = isAdmin || isGlobalMod || isPremiumMember || isVipMember;
|
|
286
|
+
console.log('[PDF-Secure][DEBUG] handleChat uid=%s groups: isAdmin=%s isGlobalMod=%s isPremium=%s isVip=%s', req.uid, isAdmin, isGlobalMod, isPremiumMember, isVipMember);
|
|
245
287
|
if (!isPremium) {
|
|
288
|
+
console.log('[PDF-Secure][DEBUG] handleChat REJECTED: not premium uid=%s', req.uid);
|
|
246
289
|
return res.status(403).json({ error: 'Bu özellik Premium/VIP üyelere özeldir.' });
|
|
247
290
|
}
|
|
248
291
|
|
|
249
292
|
const isVip = isVipMember || isAdmin;
|
|
250
293
|
const tier = isVip ? 'vip' : 'premium';
|
|
294
|
+
console.log('[PDF-Secure][DEBUG] handleChat uid=%s tier=%s isAdmin=%s', req.uid, tier, isAdmin);
|
|
295
|
+
|
|
296
|
+
// Quota check (admins bypass for testing)
|
|
297
|
+
let quotaResult;
|
|
298
|
+
if (isAdmin) {
|
|
299
|
+
const cfg = QUOTAS[tier] || QUOTAS.premium;
|
|
300
|
+
quotaResult = { allowed: true, used: 0, max: cfg.max, resetsIn: 0 };
|
|
301
|
+
console.log('[PDF-Secure][DEBUG] handleChat uid=%s ADMIN BYPASS - skipping quota/rate checks', req.uid);
|
|
302
|
+
} else {
|
|
303
|
+
// 4-hour rolling window quota check (visible to user in quota bar)
|
|
304
|
+
quotaResult = await checkQuota(req.uid, tier);
|
|
305
|
+
console.log('[PDF-Secure][DEBUG] handleChat uid=%s quotaResult: %j', req.uid, quotaResult);
|
|
306
|
+
if (!quotaResult.allowed) {
|
|
307
|
+
console.log('[PDF-Secure][DEBUG] handleChat REJECTED: quota exhausted uid=%s used=%d max=%d', req.uid, quotaResult.used, quotaResult.max);
|
|
308
|
+
return res.status(429).json({
|
|
309
|
+
error: 'Mesaj kotanız doldu.',
|
|
310
|
+
quotaExhausted: true,
|
|
311
|
+
quota: { used: quotaResult.used, max: quotaResult.max, resetsIn: quotaResult.resetsIn },
|
|
312
|
+
tier,
|
|
313
|
+
});
|
|
314
|
+
}
|
|
251
315
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
error: '
|
|
257
|
-
|
|
258
|
-
quota: { used: quotaResult.used, max: quotaResult.max, resetsIn: quotaResult.resetsIn },
|
|
259
|
-
tier,
|
|
260
|
-
});
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
// Spam rate limiting (DB-backed 60s sliding window, not shown in UI)
|
|
264
|
-
const rateResult = await checkRateLimit(req.uid, tier);
|
|
265
|
-
if (!rateResult.allowed) {
|
|
266
|
-
return res.status(429).json({ error: 'Çok hızlı mesaj gönderiyorsunuz. Lütfen biraz bekleyin.', quota: { used: quotaResult.used, max: quotaResult.max, resetsIn: quotaResult.resetsIn } });
|
|
316
|
+
// Spam rate limiting (DB-backed 60s sliding window, not shown in UI)
|
|
317
|
+
const rateResult = await checkRateLimit(req.uid, tier);
|
|
318
|
+
if (!rateResult.allowed) {
|
|
319
|
+
console.log('[PDF-Secure][DEBUG] handleChat REJECTED: rate limit uid=%s', req.uid);
|
|
320
|
+
return res.status(429).json({ error: 'Çok hızlı mesaj gönderiyorsunuz. Lütfen biraz bekleyin.', quota: { used: quotaResult.used, max: quotaResult.max, resetsIn: quotaResult.resetsIn } });
|
|
321
|
+
}
|
|
267
322
|
}
|
|
268
323
|
|
|
269
324
|
// Body validation
|
|
@@ -321,36 +376,48 @@ Controllers.handleChat = async function (req, res) {
|
|
|
321
376
|
}
|
|
322
377
|
|
|
323
378
|
try {
|
|
379
|
+
console.log('[PDF-Secure][DEBUG] handleChat calling geminiChat.chat uid=%s file=%s question="%s" historyLen=%d tier=%s detailMode=%s',
|
|
380
|
+
req.uid, safeName, trimmedQuestion.slice(0, 80), (history || []).length, tier, useDetailMode);
|
|
324
381
|
const result = await geminiChat.chat(safeName, trimmedQuestion, history || [], tier, useDetailMode);
|
|
325
382
|
// Record actual token usage after successful AI response
|
|
326
383
|
// Sanitize: clamp to [0, 1000000] to prevent NaN/negative/absurd values
|
|
384
|
+
const rawTokens = result.tokensUsed;
|
|
327
385
|
const tokensUsed = Math.max(0, Math.min(parseInt(result.tokensUsed, 10) || 0, 1000000));
|
|
386
|
+
console.log('[PDF-Secure][DEBUG] handleChat AI SUCCESS uid=%s rawTokens=%s parsedTokens=%d answerLen=%d suspicious=%s',
|
|
387
|
+
req.uid, rawTokens, tokensUsed, (result.text || '').length, result.suspicious);
|
|
328
388
|
const updatedQuota = await recordQuotaUsage(req.uid, tier, tokensUsed);
|
|
389
|
+
console.log('[PDF-Secure][DEBUG] handleChat RESPONSE uid=%s quota: used=%d max=%d', req.uid, updatedQuota.used, updatedQuota.max);
|
|
329
390
|
return res.json({
|
|
330
391
|
answer: result.text,
|
|
331
392
|
injectionWarning: result.suspicious || false,
|
|
332
393
|
quota: updatedQuota,
|
|
333
394
|
});
|
|
334
395
|
} catch (err) {
|
|
335
|
-
console.error('[PDF-Secure]
|
|
396
|
+
console.error('[PDF-Secure][DEBUG] handleChat ERROR uid=%s:', req.uid, err.message, 'status=', err.status || 'none', 'code=', err.code || 'none');
|
|
397
|
+
console.error('[PDF-Secure][DEBUG] handleChat ERROR stack:', err.stack);
|
|
336
398
|
// On error, no tokens were used — return pre-call quota
|
|
337
399
|
const quota = { used: quotaResult.used, max: quotaResult.max, resetsIn: quotaResult.resetsIn };
|
|
338
400
|
|
|
339
401
|
if (err.message === 'File not found') {
|
|
402
|
+
console.log('[PDF-Secure][DEBUG] handleChat returning 404 PDF not found');
|
|
340
403
|
return res.status(404).json({ error: 'PDF bulunamadı.', quota });
|
|
341
404
|
}
|
|
342
405
|
if (err.message === 'PDF too large for AI chat') {
|
|
406
|
+
console.log('[PDF-Secure][DEBUG] handleChat returning 413 PDF too large');
|
|
343
407
|
const sizeMsg = tier === 'premium'
|
|
344
408
|
? 'Bu PDF, Premium limiti (20MB) aşıyor. VIP üyelikle 50MB\'a kadar PDF\'lerle sohbet edebilirsiniz.'
|
|
345
409
|
: 'Bu PDF çok büyük. AI chat için maksimum dosya boyutu 50MB.';
|
|
346
410
|
return res.status(413).json({ error: sizeMsg, quota, showUpgrade: tier === 'premium' });
|
|
347
411
|
}
|
|
348
412
|
if (err.status === 429 || err.message.includes('rate limit') || err.message.includes('quota')) {
|
|
413
|
+
console.log('[PDF-Secure][DEBUG] handleChat returning 429 Gemini rate limit/quota');
|
|
349
414
|
return res.status(429).json({ error: 'AI servisi şu an yoğun. Lütfen birkaç saniye sonra tekrar deneyin.', quota });
|
|
350
415
|
}
|
|
351
416
|
if (err.status === 401 || err.status === 403 || err.message.includes('API key')) {
|
|
417
|
+
console.log('[PDF-Secure][DEBUG] handleChat returning 503 API key/auth issue');
|
|
352
418
|
return res.status(503).json({ error: 'AI servisi yapılandırma hatası. Yöneticiyle iletişime geçin.', quota });
|
|
353
419
|
}
|
|
420
|
+
console.log('[PDF-Secure][DEBUG] handleChat returning 500 generic error');
|
|
354
421
|
return res.status(500).json({ error: 'AI yanıt veremedi. Lütfen tekrar deneyin.', quota });
|
|
355
422
|
}
|
|
356
423
|
};
|
|
@@ -365,10 +432,12 @@ setInterval(() => {
|
|
|
365
432
|
}
|
|
366
433
|
}, 5 * 60 * 1000).unref();
|
|
367
434
|
Controllers.getSuggestions = async function (req, res) {
|
|
435
|
+
console.log('[PDF-Secure][DEBUG] getSuggestions START uid=%s filename=%s', req.uid, req.query?.filename);
|
|
368
436
|
if (!req.uid) {
|
|
369
437
|
return res.status(401).json({ error: 'Authentication required' });
|
|
370
438
|
}
|
|
371
439
|
if (!geminiChat.isAvailable()) {
|
|
440
|
+
console.log('[PDF-Secure][DEBUG] getSuggestions REJECTED: AI not available');
|
|
372
441
|
return res.status(503).json({ error: 'AI chat is not configured' });
|
|
373
442
|
}
|
|
374
443
|
|
|
@@ -379,6 +448,7 @@ Controllers.getSuggestions = async function (req, res) {
|
|
|
379
448
|
groups.isMember(req.uid, 'Premium'),
|
|
380
449
|
groups.isMember(req.uid, 'VIP'),
|
|
381
450
|
]);
|
|
451
|
+
console.log('[PDF-Secure][DEBUG] getSuggestions uid=%s groups: admin=%s gmod=%s premium=%s vip=%s', req.uid, isAdmin, isGlobalMod, isPremiumMember, isVipMember);
|
|
382
452
|
if (!isAdmin && !isGlobalMod && !isPremiumMember && !isVipMember) {
|
|
383
453
|
return res.status(403).json({ error: 'Premium/VIP only' });
|
|
384
454
|
}
|
|
@@ -410,11 +480,47 @@ Controllers.getSuggestions = async function (req, res) {
|
|
|
410
480
|
}
|
|
411
481
|
|
|
412
482
|
try {
|
|
483
|
+
console.log('[PDF-Secure][DEBUG] getSuggestions calling generateSuggestions file=%s tier=%s', safeName, tier);
|
|
413
484
|
const suggestions = await geminiChat.generateSuggestions(safeName, tier);
|
|
414
485
|
const quotaUsage = await getQuotaUsage(req.uid, tier);
|
|
486
|
+
console.log('[PDF-Secure][DEBUG] getSuggestions SUCCESS uid=%s suggestions=%d quota: used=%d max=%d', req.uid, suggestions.length, quotaUsage.used, quotaUsage.max);
|
|
415
487
|
return res.json({ suggestions, quota: quotaUsage });
|
|
416
488
|
} catch (err) {
|
|
417
|
-
console.error('[PDF-Secure]
|
|
489
|
+
console.error('[PDF-Secure][DEBUG] getSuggestions ERROR uid=%s:', req.uid, err.message, err.stack);
|
|
418
490
|
return res.json({ suggestions: [] });
|
|
419
491
|
}
|
|
420
492
|
};
|
|
493
|
+
|
|
494
|
+
// Admin: reset quota for a specific user or all users
|
|
495
|
+
Controllers.resetQuota = async function (req, res) {
|
|
496
|
+
console.log('[PDF-Secure][DEBUG] resetQuota called by uid=%s body=%j', req.uid, req.body);
|
|
497
|
+
if (!req.uid) {
|
|
498
|
+
return res.status(401).json({ error: 'Authentication required' });
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
const isAdmin = await groups.isMember(req.uid, 'administrators');
|
|
502
|
+
if (!isAdmin) {
|
|
503
|
+
console.log('[PDF-Secure][DEBUG] resetQuota REJECTED: uid=%s not admin', req.uid);
|
|
504
|
+
return res.status(403).json({ error: 'Admin only' });
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
const targetUid = parseInt(req.body.uid, 10);
|
|
508
|
+
|
|
509
|
+
try {
|
|
510
|
+
if (targetUid > 0) {
|
|
511
|
+
// Reset specific user's quota and rate limit
|
|
512
|
+
console.log('[PDF-Secure][DEBUG] resetQuota deleting quota+ratelimit for uid=%s (requested by admin uid=%s)', targetUid, req.uid);
|
|
513
|
+
await db.delete(`pdf-secure:quota:${targetUid}`);
|
|
514
|
+
await db.delete(`pdf-secure:ratelimit:${targetUid}`);
|
|
515
|
+
return res.json({ success: true, message: `UID ${targetUid} kotası sıfırlandı.` });
|
|
516
|
+
}
|
|
517
|
+
// No uid specified: reset the calling admin's own quota
|
|
518
|
+
console.log('[PDF-Secure][DEBUG] resetQuota deleting own quota+ratelimit for admin uid=%s', req.uid);
|
|
519
|
+
await db.delete(`pdf-secure:quota:${req.uid}`);
|
|
520
|
+
await db.delete(`pdf-secure:ratelimit:${req.uid}`);
|
|
521
|
+
return res.json({ success: true, message: 'Kendi kotanız sıfırlandı.' });
|
|
522
|
+
} catch (err) {
|
|
523
|
+
console.error('[PDF-Secure][DEBUG] resetQuota ERROR:', err.message, err.stack);
|
|
524
|
+
return res.status(500).json({ error: 'Kota sıfırlama hatası.' });
|
|
525
|
+
}
|
|
526
|
+
};
|
package/lib/gemini-chat.js
CHANGED
|
@@ -159,16 +159,18 @@ GeminiChat.setCustomPrompts = function (prompts) {
|
|
|
159
159
|
};
|
|
160
160
|
|
|
161
161
|
GeminiChat.init = function (apiKey) {
|
|
162
|
+
console.log('[PDF-Secure][DEBUG] GeminiChat.init called, apiKey=%s', apiKey ? '***' + apiKey.slice(-6) : '***EMPTY***');
|
|
162
163
|
if (!apiKey) {
|
|
163
164
|
ai = null;
|
|
165
|
+
console.log('[PDF-Secure][DEBUG] GeminiChat.init: no API key, ai=null');
|
|
164
166
|
return;
|
|
165
167
|
}
|
|
166
168
|
try {
|
|
167
169
|
const { GoogleGenAI } = require('@google/genai');
|
|
168
170
|
ai = new GoogleGenAI({ apiKey });
|
|
169
|
-
console.log('[PDF-Secure] Gemini AI client initialized');
|
|
171
|
+
console.log('[PDF-Secure][DEBUG] GeminiChat.init SUCCESS - Gemini AI client initialized');
|
|
170
172
|
} catch (err) {
|
|
171
|
-
console.error('[PDF-Secure]
|
|
173
|
+
console.error('[PDF-Secure][DEBUG] GeminiChat.init FAILED:', err.message, err.stack);
|
|
172
174
|
ai = null;
|
|
173
175
|
}
|
|
174
176
|
};
|
|
@@ -183,16 +185,20 @@ async function getOrUploadPdf(filename, tier) {
|
|
|
183
185
|
// Check file upload cache first
|
|
184
186
|
const cached = fileUploadCache.get(filename);
|
|
185
187
|
if (cached && Date.now() - cached.cachedAt < PDF_DATA_TTL) {
|
|
188
|
+
console.log('[PDF-Secure][DEBUG] getOrUploadPdf CACHED file=%s uri=%s', filename, cached.fileUri);
|
|
186
189
|
return { type: 'fileData', fileUri: cached.fileUri, mimeType: cached.mimeType };
|
|
187
190
|
}
|
|
188
191
|
|
|
189
192
|
const filePath = pdfHandler.resolveFilePath(filename);
|
|
193
|
+
console.log('[PDF-Secure][DEBUG] getOrUploadPdf file=%s resolvedPath=%s', filename, filePath);
|
|
190
194
|
if (!filePath || !fs.existsSync(filePath)) {
|
|
195
|
+
console.log('[PDF-Secure][DEBUG] getOrUploadPdf FILE NOT FOUND file=%s path=%s', filename, filePath);
|
|
191
196
|
throw new Error('File not found');
|
|
192
197
|
}
|
|
193
198
|
|
|
194
199
|
const maxSize = MAX_FILE_SIZE[tier] || MAX_FILE_SIZE.premium;
|
|
195
200
|
const stats = await fs.promises.stat(filePath);
|
|
201
|
+
console.log('[PDF-Secure][DEBUG] getOrUploadPdf file=%s size=%dKB maxSize=%dKB tier=%s', filename, Math.round(stats.size / 1024), Math.round(maxSize / 1024), tier);
|
|
196
202
|
if (stats.size > maxSize) {
|
|
197
203
|
throw new Error('PDF too large for AI chat');
|
|
198
204
|
}
|
|
@@ -211,9 +217,11 @@ async function getOrUploadPdf(filename, tier) {
|
|
|
211
217
|
for (let i = 0; i < methods.length; i++) {
|
|
212
218
|
if (uploadResult) break;
|
|
213
219
|
try {
|
|
220
|
+
console.log('[PDF-Secure][DEBUG] getOrUploadPdf trying upload method %d for %s', i + 1, filename);
|
|
214
221
|
uploadResult = await methods[i]();
|
|
222
|
+
console.log('[PDF-Secure][DEBUG] getOrUploadPdf upload method %d SUCCESS uri=%s', i + 1, uploadResult?.uri);
|
|
215
223
|
} catch (err) {
|
|
216
|
-
console.warn('[PDF-Secure]
|
|
224
|
+
console.warn('[PDF-Secure][DEBUG] getOrUploadPdf upload method %d FAILED: %s', i + 1, err.message);
|
|
217
225
|
}
|
|
218
226
|
}
|
|
219
227
|
|
|
@@ -223,8 +231,9 @@ async function getOrUploadPdf(filename, tier) {
|
|
|
223
231
|
}
|
|
224
232
|
|
|
225
233
|
// All upload methods failed — fallback to inline base64
|
|
226
|
-
console.error('[PDF-Secure]
|
|
234
|
+
console.error('[PDF-Secure][DEBUG] getOrUploadPdf ALL upload methods FAILED for %s - falling back to inline base64', filename);
|
|
227
235
|
const base64 = fileBuffer.toString('base64');
|
|
236
|
+
console.log('[PDF-Secure][DEBUG] getOrUploadPdf inline base64 size=%dKB', Math.round(base64.length / 1024));
|
|
228
237
|
return { type: 'inlineData', mimeType: 'application/pdf', data: base64 };
|
|
229
238
|
}
|
|
230
239
|
|
|
@@ -261,7 +270,9 @@ function isSummaryRequest(question, history) {
|
|
|
261
270
|
}
|
|
262
271
|
|
|
263
272
|
GeminiChat.chat = async function (filename, question, history, tier, detailMode) {
|
|
273
|
+
console.log('[PDF-Secure][DEBUG] chat START file=%s question="%s" historyLen=%d tier=%s detailMode=%s', filename, question.slice(0, 60), history.length, tier, detailMode);
|
|
264
274
|
if (!ai) {
|
|
275
|
+
console.log('[PDF-Secure][DEBUG] chat FAILED: ai is null (not initialized)');
|
|
265
276
|
throw new Error('AI chat is not configured');
|
|
266
277
|
}
|
|
267
278
|
|
|
@@ -269,12 +280,15 @@ GeminiChat.chat = async function (filename, question, history, tier, detailMode)
|
|
|
269
280
|
if (isSummaryRequest(question, history)) {
|
|
270
281
|
const cached = summaryCache.get(filename);
|
|
271
282
|
if (cached && Date.now() - cached.cachedAt < SUMMARY_TTL) {
|
|
283
|
+
console.log('[PDF-Secure][DEBUG] chat returning CACHED summary for %s (tokensUsed=0)', filename);
|
|
272
284
|
return { text: cached.text, suspicious: false, tokensUsed: 0 };
|
|
273
285
|
}
|
|
274
286
|
}
|
|
275
287
|
|
|
276
288
|
const config = TIER_CONFIG[tier] || TIER_CONFIG.premium;
|
|
289
|
+
console.log('[PDF-Secure][DEBUG] chat uploading/fetching PDF for %s', filename);
|
|
277
290
|
const pdfRef = await getOrUploadPdf(filename, tier);
|
|
291
|
+
console.log('[PDF-Secure][DEBUG] chat pdfRef type=%s fileUri=%s', pdfRef.type, pdfRef.fileUri || 'inline');
|
|
278
292
|
|
|
279
293
|
// Build conversation contents from history (trimmed to last N entries)
|
|
280
294
|
// Cost optimization: only recent messages get full text, older ones are truncated
|
|
@@ -347,35 +361,46 @@ GeminiChat.chat = async function (filename, question, history, tier, detailMode)
|
|
|
347
361
|
systemInstruction += '\n' + SECURITY_RULES;
|
|
348
362
|
}
|
|
349
363
|
|
|
364
|
+
const maxOutputTokens = (tier === 'vip' && detailMode) ? config.detailedMaxOutputTokens : config.maxOutputTokens;
|
|
365
|
+
console.log('[PDF-Secure][DEBUG] chat calling Gemini API model=%s maxOutputTokens=%d contentsLen=%d', MODEL_NAME, maxOutputTokens, fullContents.length);
|
|
366
|
+
|
|
350
367
|
const response = await ai.models.generateContent({
|
|
351
368
|
model: MODEL_NAME,
|
|
352
369
|
contents: fullContents,
|
|
353
370
|
config: {
|
|
354
371
|
systemInstruction,
|
|
355
|
-
maxOutputTokens
|
|
372
|
+
maxOutputTokens,
|
|
356
373
|
},
|
|
357
374
|
});
|
|
358
375
|
|
|
376
|
+
console.log('[PDF-Secure][DEBUG] chat Gemini API response received');
|
|
377
|
+
|
|
359
378
|
const text = response?.candidates?.[0]?.content?.parts?.[0]?.text;
|
|
360
379
|
if (!text) {
|
|
380
|
+
console.log('[PDF-Secure][DEBUG] chat EMPTY RESPONSE from Gemini. candidates=%j', response?.candidates);
|
|
361
381
|
throw new Error('Empty response from AI');
|
|
362
382
|
}
|
|
363
383
|
|
|
364
384
|
// Extract output token count — multiple fallbacks for SDK version compatibility
|
|
365
385
|
const usage = response?.usageMetadata || response?.usage_metadata || {};
|
|
386
|
+
console.log('[PDF-Secure][DEBUG] chat usageMetadata raw: %j', usage);
|
|
366
387
|
let tokensUsed = usage.candidatesTokenCount
|
|
367
388
|
|| usage.candidates_token_count
|
|
368
389
|
|| usage.outputTokenCount
|
|
369
390
|
|| usage.output_token_count
|
|
370
391
|
|| 0;
|
|
392
|
+
console.log('[PDF-Secure][DEBUG] chat token extraction: candidatesTokenCount=%s candidates_token_count=%s outputTokenCount=%s output_token_count=%s => tokensUsed=%s',
|
|
393
|
+
usage.candidatesTokenCount, usage.candidates_token_count, usage.outputTokenCount, usage.output_token_count, tokensUsed);
|
|
371
394
|
// Last resort: estimate from text length if API didn't return token count
|
|
372
395
|
// (~4 chars per token is a reasonable approximation for multilingual text)
|
|
373
396
|
if (!tokensUsed && text.length > 0) {
|
|
374
397
|
tokensUsed = Math.ceil(text.length / 4);
|
|
398
|
+
console.log('[PDF-Secure][DEBUG] chat token ESTIMATED from text length: textLen=%d => tokensUsed=%d', text.length, tokensUsed);
|
|
375
399
|
}
|
|
376
400
|
|
|
377
401
|
const result = sanitizeAiOutput(text);
|
|
378
402
|
result.tokensUsed = tokensUsed;
|
|
403
|
+
console.log('[PDF-Secure][DEBUG] chat DONE file=%s tokensUsed=%d textLen=%d suspicious=%s', filename, tokensUsed, text.length, result.suspicious);
|
|
379
404
|
|
|
380
405
|
// Cache summary responses (most expensive request type)
|
|
381
406
|
if (isSummaryRequest(question, history) && !result.suspicious) {
|
|
@@ -387,7 +412,9 @@ GeminiChat.chat = async function (filename, question, history, tier, detailMode)
|
|
|
387
412
|
|
|
388
413
|
// Generate AI-powered question suggestions for a PDF
|
|
389
414
|
GeminiChat.generateSuggestions = async function (filename, tier) {
|
|
415
|
+
console.log('[PDF-Secure][DEBUG] generateSuggestions START file=%s tier=%s', filename, tier);
|
|
390
416
|
if (!ai) {
|
|
417
|
+
console.log('[PDF-Secure][DEBUG] generateSuggestions FAILED: ai is null');
|
|
391
418
|
throw new Error('AI chat is not configured');
|
|
392
419
|
}
|
|
393
420
|
|
|
@@ -395,6 +422,7 @@ GeminiChat.generateSuggestions = async function (filename, tier) {
|
|
|
395
422
|
const cacheKey = filename + '::' + (tier || 'premium');
|
|
396
423
|
const cached = suggestionsCache.get(cacheKey);
|
|
397
424
|
if (cached && Date.now() - cached.cachedAt < SUGGESTIONS_TTL) {
|
|
425
|
+
console.log('[PDF-Secure][DEBUG] generateSuggestions returning CACHED for %s', cacheKey);
|
|
398
426
|
return cached.suggestions;
|
|
399
427
|
}
|
|
400
428
|
|
package/library.js
CHANGED
|
@@ -77,12 +77,23 @@ plugin.init = async (params) => {
|
|
|
77
77
|
// AI suggestions endpoint (Premium/VIP only)
|
|
78
78
|
router.get('/api/v3/plugins/pdf-secure/suggestions', controllers.getSuggestions);
|
|
79
79
|
|
|
80
|
+
// Admin: reset user quota (for testing/support)
|
|
81
|
+
router.post('/api/v3/plugins/pdf-secure/reset-quota', controllers.resetQuota);
|
|
82
|
+
|
|
80
83
|
// Load plugin settings
|
|
81
84
|
pluginSettings = await meta.settings.get('pdf-secure') || {};
|
|
85
|
+
console.log('[PDF-Secure][DEBUG] Plugin settings loaded. Keys:', Object.keys(pluginSettings).join(', '));
|
|
86
|
+
console.log('[PDF-Secure][DEBUG] geminiApiKey=%s, quotaPremium=%s, quotaVip=%s, quotaWindow=%s',
|
|
87
|
+
pluginSettings.geminiApiKey ? '***SET(' + pluginSettings.geminiApiKey.length + ' chars)' : '***EMPTY***',
|
|
88
|
+
pluginSettings.quotaPremiumTokens || 'default',
|
|
89
|
+
pluginSettings.quotaVipTokens || 'default',
|
|
90
|
+
pluginSettings.quotaWindowHours || 'default');
|
|
82
91
|
|
|
83
92
|
// Initialize Gemini AI chat (if API key is configured)
|
|
84
93
|
if (pluginSettings.geminiApiKey) {
|
|
85
94
|
geminiChat.init(pluginSettings.geminiApiKey);
|
|
95
|
+
} else {
|
|
96
|
+
console.log('[PDF-Secure][DEBUG] WARNING: No Gemini API key configured! AI chat will be disabled.');
|
|
86
97
|
}
|
|
87
98
|
|
|
88
99
|
// Apply admin-configured quota settings
|
package/package.json
CHANGED
package/static/lib/admin.js
CHANGED
|
@@ -34,6 +34,31 @@ define('admin/plugins/pdf-secure', ['settings', 'alerts'], function (Settings, a
|
|
|
34
34
|
$('#deselectAllCats').on('click', function () {
|
|
35
35
|
$('#categoryCheckboxes input[type="checkbox"]').prop('checked', false);
|
|
36
36
|
});
|
|
37
|
+
|
|
38
|
+
// Quota reset button
|
|
39
|
+
$('#resetQuotaBtn').on('click', function () {
|
|
40
|
+
var uid = parseInt($('#resetQuotaUid').val(), 10) || 0;
|
|
41
|
+
var btn = $(this);
|
|
42
|
+
btn.prop('disabled', true);
|
|
43
|
+
$('#resetQuotaResult').text('Sifirlaniyor...').css('color', '#6c757d');
|
|
44
|
+
|
|
45
|
+
$.ajax({
|
|
46
|
+
url: config.relative_path + '/api/v3/plugins/pdf-secure/reset-quota',
|
|
47
|
+
type: 'POST',
|
|
48
|
+
contentType: 'application/json',
|
|
49
|
+
headers: { 'x-csrf-token': config.csrf_token },
|
|
50
|
+
data: JSON.stringify(uid > 0 ? { uid: uid } : {}),
|
|
51
|
+
success: function (data) {
|
|
52
|
+
$('#resetQuotaResult').text(data.message || 'Basarili!').css('color', '#198754');
|
|
53
|
+
btn.prop('disabled', false);
|
|
54
|
+
},
|
|
55
|
+
error: function (xhr) {
|
|
56
|
+
var msg = (xhr.responseJSON && xhr.responseJSON.error) || 'Hata olustu.';
|
|
57
|
+
$('#resetQuotaResult').text(msg).css('color', '#dc3545');
|
|
58
|
+
btn.prop('disabled', false);
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
});
|
|
37
62
|
};
|
|
38
63
|
|
|
39
64
|
function loadCategories() {
|
|
@@ -153,11 +153,27 @@
|
|
|
153
153
|
<div class="form-text" style="font-size:11px;">Varsayilan: 4 saat. Kota bu sure icerisinde sifirlanir.</div>
|
|
154
154
|
</div>
|
|
155
155
|
|
|
156
|
-
<div class="alert alert-light border d-flex gap-2 mb-
|
|
156
|
+
<div class="alert alert-light border d-flex gap-2 mb-3" role="alert" style="font-size:12px;">
|
|
157
157
|
<svg viewBox="0 0 24 24" style="width:16px;height:16px;fill:#0d6efd;flex-shrink:0;margin-top:1px;"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z"/></svg>
|
|
158
158
|
<div>
|
|
159
159
|
Sadece output (yanit) tokenleri sayilir. PDF ve soru tokenleri dahil degildir.<br>
|
|
160
|
-
Ortalama mesaj ~1000-2000 token, uzun ozet ~3000-4000 token tuketir
|
|
160
|
+
Ortalama mesaj ~1000-2000 token, uzun ozet ~3000-4000 token tuketir.<br>
|
|
161
|
+
<strong>Not:</strong> Admin kullanicilari kota sinirlamasindan muaftir.
|
|
162
|
+
</div>
|
|
163
|
+
</div>
|
|
164
|
+
|
|
165
|
+
<hr class="my-3">
|
|
166
|
+
<h6 class="fw-semibold mb-3" style="font-size:13px;">Kota Sifirlama</h6>
|
|
167
|
+
<div class="row g-3 align-items-end">
|
|
168
|
+
<div class="col-auto">
|
|
169
|
+
<label class="form-label fw-medium" for="resetQuotaUid" style="font-size:13px;">Kullanici UID</label>
|
|
170
|
+
<input type="number" id="resetQuotaUid" class="form-control form-control-sm" placeholder="Bos = kendi kotam" min="0" style="width:150px;">
|
|
171
|
+
</div>
|
|
172
|
+
<div class="col-auto">
|
|
173
|
+
<button type="button" id="resetQuotaBtn" class="btn btn-sm btn-outline-warning">Kotayi Sifirla</button>
|
|
174
|
+
</div>
|
|
175
|
+
<div class="col-auto">
|
|
176
|
+
<span id="resetQuotaResult" style="font-size:12px;"></span>
|
|
161
177
|
</div>
|
|
162
178
|
</div>
|
|
163
179
|
</div>
|