nodebb-plugin-pdf-secure2 1.4.3 → 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.
@@ -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
 
@@ -26,6 +27,12 @@ const QUOTAS = {
26
27
 
27
28
  // Apply admin-configured quota settings (called from library.js on init)
28
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
+ }));
29
36
  if (!settings) return;
30
37
  const premiumTokens = parseInt(settings.quotaPremiumTokens, 10);
31
38
  const vipTokens = parseInt(settings.quotaVipTokens, 10);
@@ -38,6 +45,7 @@ Controllers.initQuotaSettings = function (settings) {
38
45
  QUOTAS.premium.window = windowMs;
39
46
  QUOTAS.vip.window = windowMs;
40
47
  }
48
+ console.log('[PDF-Secure][DEBUG] Final QUOTAS after init:', JSON.stringify(QUOTAS));
41
49
  };
42
50
 
43
51
  // DB-backed sliding window rate limiter
@@ -53,15 +61,17 @@ async function checkRateLimit(uid, tier) {
53
61
  await db.sortedSetRemoveRangeByScore(key, '-inf', windowStart);
54
62
  // Count remaining entries in window
55
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);
56
65
  if (count >= rateConfig.max) {
66
+ console.log('[PDF-Secure][DEBUG] Rate limit BLOCKED uid=%s (%d >= %d)', uid, count, rateConfig.max);
57
67
  return { allowed: false, used: count, max: rateConfig.max };
58
68
  }
59
69
  // Add current request (unique member via timestamp + random suffix)
60
70
  await db.sortedSetAdd(key, now, `${now}:${crypto.randomBytes(4).toString('hex')}`);
61
71
  return { allowed: true, used: count + 1, max: rateConfig.max };
62
72
  } catch (err) {
63
- // Graceful degradation: allow request if DB fails
64
- return { allowed: true, used: 0, max: rateConfig.max };
73
+ console.error('[PDF-Secure][DEBUG] Rate limit DB ERROR:', err.message, err.stack);
74
+ return { allowed: false, used: 0, max: rateConfig.max };
65
75
  }
66
76
  }
67
77
 
@@ -70,21 +80,29 @@ async function checkRateLimit(uid, tier) {
70
80
  function sumTokensFromMembers(members) {
71
81
  let total = 0;
72
82
  for (const m of members) {
73
- const parts = (typeof m === 'string' ? m : m.value || '').split(':');
74
- 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
+ }
75
91
  }
76
92
  return total;
77
93
  }
78
94
 
79
95
  // Token-based 4-hour sliding window quota (DB-backed sorted set)
80
96
  // Read-only check — does NOT add entries (call recordQuotaUsage after AI response)
81
- // Returns { allowed, used, max, resetsIn }
97
+ // Returns { allowed, used, max, resetsIn, dbError }
82
98
  async function checkQuota(uid, tier) {
83
99
  const cfg = QUOTAS[tier] || QUOTAS.premium;
84
100
  const key = `pdf-secure:quota:${uid}`;
85
101
  const now = Date.now();
86
102
  const windowStart = now - cfg.window;
87
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
+
88
106
  try {
89
107
  // Remove expired entries outside the 4-hour window
90
108
  await db.sortedSetRemoveRangeByScore(key, '-inf', windowStart);
@@ -92,21 +110,33 @@ async function checkQuota(uid, tier) {
92
110
  const members = await db.getSortedSetRange(key, 0, -1);
93
111
  const totalTokens = sumTokensFromMembers(members);
94
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
+
95
119
  // Calculate resetsIn from oldest entry
96
120
  let resetsIn = 0;
97
121
  if (members.length > 0) {
98
122
  const oldest = await db.getSortedSetRangeWithScores(key, 0, 0);
99
123
  if (oldest && oldest.length > 0) {
100
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));
101
126
  }
102
127
  }
103
128
 
104
129
  if (totalTokens >= cfg.max) {
130
+ console.log('[PDF-Secure][DEBUG] checkQuota BLOCKED uid=%s totalTokens=%d >= max=%d', uid, totalTokens, cfg.max);
105
131
  return { allowed: false, used: totalTokens, max: cfg.max, resetsIn };
106
132
  }
133
+ console.log('[PDF-Secure][DEBUG] checkQuota ALLOWED uid=%s totalTokens=%d < max=%d', uid, totalTokens, cfg.max);
107
134
  return { allowed: true, used: totalTokens, max: cfg.max, resetsIn };
108
135
  } catch (err) {
109
- return { allowed: true, 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 };
110
140
  }
111
141
  }
112
142
 
@@ -116,10 +146,13 @@ async function recordQuotaUsage(uid, tier, tokensUsed) {
116
146
  const cfg = QUOTAS[tier] || QUOTAS.premium;
117
147
  const key = `pdf-secure:quota:${uid}`;
118
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);
119
152
 
120
153
  try {
121
154
  // Add entry with token count encoded in member string
122
- await db.sortedSetAdd(key, now, `${now}:${crypto.randomBytes(4).toString('hex')}:${tokensUsed}`);
155
+ await db.sortedSetAdd(key, now, member);
123
156
 
124
157
  // Re-read total for accurate response
125
158
  const members = await db.getSortedSetRange(key, 0, -1);
@@ -133,8 +166,10 @@ async function recordQuotaUsage(uid, tier, tokensUsed) {
133
166
  }
134
167
  }
135
168
 
169
+ console.log('[PDF-Secure][DEBUG] recordQuotaUsage uid=%s afterRecord: totalTokens=%d members=%d max=%d', uid, totalTokens, members.length, cfg.max);
136
170
  return { used: totalTokens, max: cfg.max, resetsIn };
137
171
  } catch (err) {
172
+ console.error('[PDF-Secure][DEBUG] recordQuotaUsage DB ERROR uid=%s:', uid, err.message, err.stack);
138
173
  return { used: 0, max: cfg.max, resetsIn: 0 };
139
174
  }
140
175
  }
@@ -146,6 +181,8 @@ async function getQuotaUsage(uid, tier) {
146
181
  const now = Date.now();
147
182
  const windowStart = now - cfg.window;
148
183
 
184
+ console.log('[PDF-Secure][DEBUG] getQuotaUsage uid=%s tier=%s', uid, tier);
185
+
149
186
  try {
150
187
  await db.sortedSetRemoveRangeByScore(key, '-inf', windowStart);
151
188
  const members = await db.getSortedSetRange(key, 0, -1);
@@ -157,8 +194,10 @@ async function getQuotaUsage(uid, tier) {
157
194
  resetsIn = Math.max(0, (oldest[0].score + cfg.window) - now);
158
195
  }
159
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);
160
198
  return { used: totalTokens, max: cfg.max, resetsIn };
161
199
  } catch (err) {
200
+ console.error('[PDF-Secure][DEBUG] getQuotaUsage DB ERROR uid=%s:', uid, err.message, err.stack);
162
201
  return { used: 0, max: cfg.max, resetsIn: 0 };
163
202
  }
164
203
  }
@@ -222,13 +261,17 @@ Controllers.servePdfBinary = async function (req, res) {
222
261
  };
223
262
 
224
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
+
225
266
  // Authentication gate
226
267
  if (!req.uid) {
268
+ console.log('[PDF-Secure][DEBUG] handleChat REJECTED: not authenticated');
227
269
  return res.status(401).json({ error: 'Authentication required' });
228
270
  }
229
271
 
230
272
  // Check AI availability
231
273
  if (!geminiChat.isAvailable()) {
274
+ console.log('[PDF-Secure][DEBUG] handleChat REJECTED: AI not available (no API key?)');
232
275
  return res.status(503).json({ error: 'AI chat is not configured' });
233
276
  }
234
277
 
@@ -240,32 +283,47 @@ Controllers.handleChat = async function (req, res) {
240
283
  groups.isMember(req.uid, 'VIP'),
241
284
  ]);
242
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);
243
287
  if (!isPremium) {
288
+ console.log('[PDF-Secure][DEBUG] handleChat REJECTED: not premium uid=%s', req.uid);
244
289
  return res.status(403).json({ error: 'Bu özellik Premium/VIP üyelere özeldir.' });
245
290
  }
246
291
 
247
292
  const isVip = isVipMember || isAdmin;
248
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
+ }
249
315
 
250
- // 4-hour rolling window quota check (visible to user in quota bar)
251
- const quotaResult = await checkQuota(req.uid, tier);
252
- if (!quotaResult.allowed) {
253
- return res.status(429).json({
254
- error: 'Mesaj kotanız doldu.',
255
- quotaExhausted: true,
256
- quota: { used: quotaResult.used, max: quotaResult.max, resetsIn: quotaResult.resetsIn },
257
- tier,
258
- });
259
- }
260
-
261
- // Spam rate limiting (DB-backed 60s sliding window, not shown in UI)
262
- const rateResult = await checkRateLimit(req.uid, tier);
263
- if (!rateResult.allowed) {
264
- 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
+ }
265
322
  }
266
323
 
267
324
  // Body validation
268
- const { filename, question, history } = req.body;
325
+ const { filename, question, history, tid, detailMode } = req.body;
326
+ const useDetailMode = tier === 'vip' && detailMode === true;
269
327
 
270
328
  if (!filename || typeof filename !== 'string') {
271
329
  return res.status(400).json({ error: 'Missing or invalid filename' });
@@ -275,6 +333,12 @@ Controllers.handleChat = async function (req, res) {
275
333
  return res.status(400).json({ error: 'Invalid filename' });
276
334
  }
277
335
 
336
+ // Topic-level access control
337
+ const accessResult = await topicAccess.validate(req.uid, tid, safeName);
338
+ if (!accessResult.allowed) {
339
+ return res.status(403).json({ error: accessResult.reason || 'Access denied' });
340
+ }
341
+
278
342
  const trimmedQuestion = typeof question === 'string' ? question.trim() : '';
279
343
  if (!trimmedQuestion || trimmedQuestion.length > 2000) {
280
344
  return res.status(400).json({ error: 'Question is required (max 2000 characters)' });
@@ -312,36 +376,48 @@ Controllers.handleChat = async function (req, res) {
312
376
  }
313
377
 
314
378
  try {
315
- const result = await geminiChat.chat(safeName, trimmedQuestion, history || [], tier);
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);
381
+ const result = await geminiChat.chat(safeName, trimmedQuestion, history || [], tier, useDetailMode);
316
382
  // Record actual token usage after successful AI response
317
383
  // Sanitize: clamp to [0, 1000000] to prevent NaN/negative/absurd values
384
+ const rawTokens = result.tokensUsed;
318
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);
319
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);
320
390
  return res.json({
321
391
  answer: result.text,
322
392
  injectionWarning: result.suspicious || false,
323
393
  quota: updatedQuota,
324
394
  });
325
395
  } catch (err) {
326
- 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);
327
398
  // On error, no tokens were used — return pre-call quota
328
399
  const quota = { used: quotaResult.used, max: quotaResult.max, resetsIn: quotaResult.resetsIn };
329
400
 
330
401
  if (err.message === 'File not found') {
402
+ console.log('[PDF-Secure][DEBUG] handleChat returning 404 PDF not found');
331
403
  return res.status(404).json({ error: 'PDF bulunamadı.', quota });
332
404
  }
333
405
  if (err.message === 'PDF too large for AI chat') {
406
+ console.log('[PDF-Secure][DEBUG] handleChat returning 413 PDF too large');
334
407
  const sizeMsg = tier === 'premium'
335
408
  ? 'Bu PDF, Premium limiti (20MB) aşıyor. VIP üyelikle 50MB\'a kadar PDF\'lerle sohbet edebilirsiniz.'
336
409
  : 'Bu PDF çok büyük. AI chat için maksimum dosya boyutu 50MB.';
337
410
  return res.status(413).json({ error: sizeMsg, quota, showUpgrade: tier === 'premium' });
338
411
  }
339
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');
340
414
  return res.status(429).json({ error: 'AI servisi şu an yoğun. Lütfen birkaç saniye sonra tekrar deneyin.', quota });
341
415
  }
342
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');
343
418
  return res.status(503).json({ error: 'AI servisi yapılandırma hatası. Yöneticiyle iletişime geçin.', quota });
344
419
  }
420
+ console.log('[PDF-Secure][DEBUG] handleChat returning 500 generic error');
345
421
  return res.status(500).json({ error: 'AI yanıt veremedi. Lütfen tekrar deneyin.', quota });
346
422
  }
347
423
  };
@@ -356,10 +432,12 @@ setInterval(() => {
356
432
  }
357
433
  }, 5 * 60 * 1000).unref();
358
434
  Controllers.getSuggestions = async function (req, res) {
435
+ console.log('[PDF-Secure][DEBUG] getSuggestions START uid=%s filename=%s', req.uid, req.query?.filename);
359
436
  if (!req.uid) {
360
437
  return res.status(401).json({ error: 'Authentication required' });
361
438
  }
362
439
  if (!geminiChat.isAvailable()) {
440
+ console.log('[PDF-Secure][DEBUG] getSuggestions REJECTED: AI not available');
363
441
  return res.status(503).json({ error: 'AI chat is not configured' });
364
442
  }
365
443
 
@@ -370,6 +448,7 @@ Controllers.getSuggestions = async function (req, res) {
370
448
  groups.isMember(req.uid, 'Premium'),
371
449
  groups.isMember(req.uid, 'VIP'),
372
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);
373
452
  if (!isAdmin && !isGlobalMod && !isPremiumMember && !isVipMember) {
374
453
  return res.status(403).json({ error: 'Premium/VIP only' });
375
454
  }
@@ -385,7 +464,7 @@ Controllers.getSuggestions = async function (req, res) {
385
464
  }
386
465
  suggestionsRateLimit.set(req.uid, now);
387
466
 
388
- const { filename } = req.query;
467
+ const { filename, tid } = req.query;
389
468
  if (!filename || typeof filename !== 'string') {
390
469
  return res.status(400).json({ error: 'Missing filename' });
391
470
  }
@@ -394,12 +473,54 @@ Controllers.getSuggestions = async function (req, res) {
394
473
  return res.status(400).json({ error: 'Invalid filename' });
395
474
  }
396
475
 
476
+ // Topic-level access control
477
+ const accessResult = await topicAccess.validate(req.uid, tid, safeName);
478
+ if (!accessResult.allowed) {
479
+ return res.status(403).json({ error: accessResult.reason || 'Access denied' });
480
+ }
481
+
397
482
  try {
398
- const suggestions = await geminiChat.generateSuggestions(safeName);
483
+ console.log('[PDF-Secure][DEBUG] getSuggestions calling generateSuggestions file=%s tier=%s', safeName, tier);
484
+ const suggestions = await geminiChat.generateSuggestions(safeName, tier);
399
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);
400
487
  return res.json({ suggestions, quota: quotaUsage });
401
488
  } catch (err) {
402
- console.error('[PDF-Secure] Suggestions error:', err.message);
489
+ console.error('[PDF-Secure][DEBUG] getSuggestions ERROR uid=%s:', req.uid, err.message, err.stack);
403
490
  return res.json({ suggestions: [] });
404
491
  }
405
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
+ };