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.
- package/.claude/settings.local.json +6 -1
- package/lib/controllers.js +149 -28
- package/lib/gemini-chat.js +480 -426
- package/lib/nonce-store.js +4 -4
- package/lib/pdf-handler.js +0 -1
- package/lib/topic-access.js +96 -0
- package/library.js +70 -5
- package/package.json +1 -1
- package/plugin.json +4 -0
- package/static/lib/admin.js +25 -0
- package/static/lib/main.js +2 -73
- package/static/templates/admin/plugins/pdf-secure.tpl +18 -2
- package/static/viewer-app.js +18 -62
- package/static/viewer.html +257 -55
|
@@ -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
|
|
|
@@ -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
|
-
|
|
64
|
-
return { allowed:
|
|
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
|
|
74
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
error: '
|
|
255
|
-
|
|
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
|
-
|
|
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]
|
|
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
|
-
|
|
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]
|
|
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
|
+
};
|