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.
@@ -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 error:', err.message);
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 parts = (typeof m === 'string' ? m : m.value || '').split(':');
75
- total += parseInt(parts[2], 10) || 0;
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] Quota check DB error:', err.message);
111
- return { allowed: false, used: 0, max: cfg.max, resetsIn: 0 };
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, `${now}:${crypto.randomBytes(4).toString('hex')}:${tokensUsed}`);
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
- // 4-hour rolling window quota check (visible to user in quota bar)
253
- const quotaResult = await checkQuota(req.uid, tier);
254
- if (!quotaResult.allowed) {
255
- return res.status(429).json({
256
- error: 'Mesaj kotanız doldu.',
257
- quotaExhausted: true,
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] Chat error:', err.message, err.status || '', err.code || '');
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] Suggestions error:', err.message);
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
+ };
@@ -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] Failed to initialize Gemini client:', err.message);
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] File API upload method', i + 1, 'failed:', err.message);
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] File API upload failed for', filename, '- falling back to inline base64');
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: (tier === 'vip' && detailMode) ? config.detailedMaxOutputTokens : config.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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-pdf-secure2",
3
- "version": "1.5.0",
3
+ "version": "1.5.1",
4
4
  "description": "Secure PDF viewer plugin for NodeBB - prevents downloading, enables canvas-only rendering with Premium group support",
5
5
  "main": "library.js",
6
6
  "repository": {
@@ -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-0" role="alert" style="font-size:12px;">
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>