@kaikybrofc/omnizap-system 2.3.4 → 2.3.6

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.
Files changed (29) hide show
  1. package/.env.example +541 -468
  2. package/.github/workflows/codeql.yml +101 -0
  3. package/README.md +14 -14
  4. package/app/modules/stickerModule/stickerCommand.js +1 -6
  5. package/app/modules/stickerModule/stickerTextCommand.js +1 -6
  6. package/ml/clip_classifier/requirements.txt +2 -2
  7. package/package.json +1 -1
  8. package/public/index.html +8 -8
  9. package/public/js/apps/homeApp.js +22 -18
  10. package/public/js/apps/loginApp.js +3 -1
  11. package/public/js/apps/stickersApp.js +145 -319
  12. package/public/js/apps/userProfileApp.js +0 -9
  13. package/public/user/index.html +224 -120
  14. package/server/controllers/admin/adminBanService.js +138 -0
  15. package/server/controllers/admin/adminPanelHandlers.js +1965 -0
  16. package/server/controllers/{systemAdminController.js → admin/systemAdminController.js} +2 -2
  17. package/server/controllers/{stickerCatalogController.js → sticker/stickerCatalogController.js} +129 -2116
  18. package/server/controllers/userController.js +1 -1
  19. package/server/routes/admin/systemAdminRouter.js +1 -1
  20. package/server/routes/indexRouter.js +3 -3
  21. package/server/routes/{stickerCatalog → sticker}/stickerApiRouter.js +1 -1
  22. package/server/routes/{stickerCatalog → sticker}/stickerDataRouter.js +1 -1
  23. package/server/routes/{stickerCatalog → sticker}/stickerSiteRouter.js +1 -1
  24. /package/server/controllers/{stickerCatalog → sticker}/nonCatalogHandlers.js +0 -0
  25. /package/server/routes/{stickerCatalog → sticker}/catalogHandlers/catalogAdminHttp.js +0 -0
  26. /package/server/routes/{stickerCatalog → sticker}/catalogHandlers/catalogAuthHttp.js +0 -0
  27. /package/server/routes/{stickerCatalog → sticker}/catalogHandlers/catalogPublicHttp.js +0 -0
  28. /package/server/routes/{stickerCatalog → sticker}/catalogHandlers/catalogUploadHttp.js +0 -0
  29. /package/server/routes/{stickerCatalog → sticker}/catalogRouter.js +0 -0
@@ -0,0 +1,1965 @@
1
+ import { randomUUID, randomBytes, scryptSync, timingSafeEqual } from 'node:crypto';
2
+ import { URLSearchParams } from 'node:url';
3
+
4
+ export const createStickerCatalogAdminHandlers = ({ executeQuery, tables, logger, sendJson, readJsonBody, parseCookies, getCookieValuesFromRequest, appendSetCookie, buildCookieString, sanitizeText, normalizeGoogleSubject, normalizeEmail, normalizeJid, toIsoOrNull, toWhatsAppPhoneDigits, mapGoogleSessionResponseData, resolveGoogleWebSessionFromRequest, revokeGoogleWebSessionsByIdentity, getMarketplaceGlobalStatsCached, getSystemSummaryCached, getFeatureFlagsSnapshot, refreshFeatureFlags, listAdminBans, createAdminBanRecord, revokeAdminBanRecord, normalizeVisitPath, stickerWebPath, findStickerPackByPackKey, stickerPackService, buildManagedPackResponseData, sendManagedMutationStatus, sendManagedPackMutationStatus, deleteManagedPackWithCleanup, mapStickerPackWebManageError, cleanupOrphanStickerAssets, invalidateStickerCatalogDerivedCaches }) => {
5
+ const TABLES = tables;
6
+ const STICKER_WEB_PATH = String(stickerWebPath || '/stickers').trim() || '/stickers';
7
+
8
+ const ADMIN_PANEL_EMAIL = String(process.env.ADM_EMAIL || '')
9
+ .trim()
10
+ .toLowerCase();
11
+ const ADMIN_PANEL_PASSWORD = String(process.env.ADM_PANEL_PASSWORD || process.env.ADM_PANEL || '').trim();
12
+ const ADMIN_PANEL_ENABLED = Boolean(ADMIN_PANEL_EMAIL && ADMIN_PANEL_PASSWORD);
13
+ const ADMIN_PANEL_SESSION_TTL_MS = Math.max(10 * 60 * 1000, Number(process.env.ADM_PANEL_SESSION_TTL_MS) || 12 * 60 * 60 * 1000);
14
+ const ADMIN_MODERATOR_PASSWORD_MIN_LENGTH = Math.max(6, Number(process.env.ADM_MODERATOR_PASSWORD_MIN_LENGTH) || 8);
15
+ const ADMIN_PANEL_SESSION_COOKIE_NAME = 'omnizap_admin_panel_session';
16
+
17
+ const adminPanelSessionMap = new Map();
18
+ let adminPanelSessionPruneAt = 0;
19
+
20
+ const constantTimeStringEqual = (a, b) => {
21
+ const left = Buffer.from(String(a || ''), 'utf8');
22
+ const right = Buffer.from(String(b || ''), 'utf8');
23
+ if (left.length !== right.length) return false;
24
+ try {
25
+ return timingSafeEqual(left, right);
26
+ } catch {
27
+ return false;
28
+ }
29
+ };
30
+ const normalizeAdminPanelRole = (value, fallback = 'owner') => {
31
+ const normalized = String(value || '')
32
+ .trim()
33
+ .toLowerCase();
34
+ if (normalized === 'moderator') return 'moderator';
35
+ if (normalized === 'owner') return 'owner';
36
+ return fallback;
37
+ };
38
+ const hashAdminModeratorPassword = (password) => {
39
+ const normalized = String(password || '');
40
+ const salt = randomBytes(16).toString('hex');
41
+ const hash = scryptSync(normalized, salt, 64).toString('hex');
42
+ return `scrypt$${salt}$${hash}`;
43
+ };
44
+ const verifyAdminModeratorPassword = (password, encodedHash) => {
45
+ const raw = String(encodedHash || '').trim();
46
+ if (!raw) return false;
47
+ const parts = raw.split('$');
48
+ if (parts.length !== 3 || parts[0] !== 'scrypt') return false;
49
+ const salt = String(parts[1] || '').trim();
50
+ const expectedHex = String(parts[2] || '').trim();
51
+ if (!salt || !expectedHex) return false;
52
+ let expectedBuffer;
53
+ try {
54
+ expectedBuffer = Buffer.from(expectedHex, 'hex');
55
+ } catch {
56
+ return false;
57
+ }
58
+ if (!expectedBuffer.length) return false;
59
+ const derived = scryptSync(String(password || ''), salt, expectedBuffer.length);
60
+ try {
61
+ return timingSafeEqual(expectedBuffer, derived);
62
+ } catch {
63
+ return false;
64
+ }
65
+ };
66
+ const mapAdminModeratorRow = (row) => {
67
+ if (!row || typeof row !== 'object') return null;
68
+ return {
69
+ google_sub: normalizeGoogleSubject(row.google_sub),
70
+ email: normalizeEmail(row.email),
71
+ owner_jid: normalizeJid(row.owner_jid) || null,
72
+ name: sanitizeText(row.name || '', 120, { allowEmpty: true }) || null,
73
+ created_by_google_sub: normalizeGoogleSubject(row.created_by_google_sub),
74
+ created_by_email: normalizeEmail(row.created_by_email),
75
+ updated_by_google_sub: normalizeGoogleSubject(row.updated_by_google_sub),
76
+ updated_by_email: normalizeEmail(row.updated_by_email),
77
+ last_login_at: toIsoOrNull(row.last_login_at),
78
+ created_at: toIsoOrNull(row.created_at),
79
+ updated_at: toIsoOrNull(row.updated_at),
80
+ revoked_at: toIsoOrNull(row.revoked_at),
81
+ active: !row.revoked_at,
82
+ };
83
+ };
84
+
85
+ const listAdminModerators = async ({ activeOnly = false, limit = 200 } = {}) => {
86
+ const safeLimit = Math.max(1, Math.min(500, Number(limit || 200)));
87
+ const rows = await executeQuery(
88
+ `SELECT google_sub, email, owner_jid, name, created_by_google_sub, created_by_email, updated_by_google_sub, updated_by_email,
89
+ last_login_at, created_at, updated_at, revoked_at
90
+ FROM ${TABLES.STICKER_WEB_ADMIN_MODERATOR}
91
+ ${activeOnly ? 'WHERE revoked_at IS NULL' : ''}
92
+ ORDER BY updated_at DESC
93
+ LIMIT ${safeLimit}`,
94
+ );
95
+ return (Array.isArray(rows) ? rows : []).map(mapAdminModeratorRow).filter(Boolean);
96
+ };
97
+
98
+ const findAdminModeratorByGoogleSub = async (googleSub, { activeOnly = false } = {}) => {
99
+ const normalizedSub = normalizeGoogleSubject(googleSub);
100
+ if (!normalizedSub) return null;
101
+ const rows = await executeQuery(
102
+ `SELECT *
103
+ FROM ${TABLES.STICKER_WEB_ADMIN_MODERATOR}
104
+ WHERE google_sub = ?
105
+ ${activeOnly ? 'AND revoked_at IS NULL' : ''}
106
+ LIMIT 1`,
107
+ [normalizedSub],
108
+ );
109
+ return Array.isArray(rows) && rows[0] ? rows[0] : null;
110
+ };
111
+
112
+ const resolveKnownGoogleUserForModerator = async ({ googleSub = '', email = '', ownerJid = '' } = {}) => {
113
+ const normalizedSub = normalizeGoogleSubject(googleSub);
114
+ const normalizedEmail = normalizeEmail(email);
115
+ const normalizedOwnerJid = normalizeJid(ownerJid) || '';
116
+ const clauses = [];
117
+ const params = [];
118
+
119
+ if (normalizedSub) {
120
+ clauses.push('google_sub = ?');
121
+ params.push(normalizedSub);
122
+ }
123
+ if (normalizedEmail) {
124
+ clauses.push('email = ?');
125
+ params.push(normalizedEmail);
126
+ }
127
+ if (normalizedOwnerJid) {
128
+ clauses.push('owner_jid = ?');
129
+ params.push(normalizedOwnerJid);
130
+ }
131
+ if (!clauses.length) return null;
132
+
133
+ const rows = await executeQuery(
134
+ `SELECT google_sub, email, owner_jid, name
135
+ FROM ${TABLES.STICKER_WEB_GOOGLE_USER}
136
+ WHERE ${clauses.join(' OR ')}
137
+ ORDER BY COALESCE(last_seen_at, last_login_at, updated_at, created_at) DESC
138
+ LIMIT 1`,
139
+ params,
140
+ );
141
+ const row = Array.isArray(rows) ? rows[0] : null;
142
+ if (!row) return null;
143
+ const resolvedGoogleSub = normalizeGoogleSubject(row.google_sub);
144
+ const resolvedEmail = normalizeEmail(row.email);
145
+ const resolvedOwnerJid = normalizeJid(row.owner_jid) || '';
146
+ if (!resolvedGoogleSub || !resolvedEmail || !resolvedOwnerJid) return null;
147
+ return {
148
+ google_sub: resolvedGoogleSub,
149
+ email: resolvedEmail,
150
+ owner_jid: resolvedOwnerJid,
151
+ name: sanitizeText(row.name || '', 120, { allowEmpty: true }) || null,
152
+ };
153
+ };
154
+
155
+ const findActiveAdminModeratorForIdentity = async ({ googleSub = '', email = '', ownerJid = '' } = {}) => {
156
+ const normalizedSub = normalizeGoogleSubject(googleSub);
157
+ const normalizedEmail = normalizeEmail(email);
158
+ const normalizedOwnerJid = normalizeJid(ownerJid) || '';
159
+ const clauses = [];
160
+ const params = [];
161
+ if (normalizedSub) {
162
+ clauses.push('google_sub = ?');
163
+ params.push(normalizedSub);
164
+ }
165
+ if (normalizedEmail) {
166
+ clauses.push('email = ?');
167
+ params.push(normalizedEmail);
168
+ }
169
+ if (normalizedOwnerJid) {
170
+ clauses.push('owner_jid = ?');
171
+ params.push(normalizedOwnerJid);
172
+ }
173
+ if (!clauses.length) return null;
174
+
175
+ const rows = await executeQuery(
176
+ `SELECT *
177
+ FROM ${TABLES.STICKER_WEB_ADMIN_MODERATOR}
178
+ WHERE revoked_at IS NULL
179
+ AND (${clauses.join(' OR ')})
180
+ ORDER BY updated_at DESC
181
+ LIMIT 1`,
182
+ params,
183
+ );
184
+ return Array.isArray(rows) && rows[0] ? rows[0] : null;
185
+ };
186
+
187
+ const pruneModeratorAdminPanelSessions = ({ googleSub = '', email = '', ownerJid = '' } = {}) => {
188
+ const normalizedSub = normalizeGoogleSubject(googleSub);
189
+ const normalizedEmail = normalizeEmail(email);
190
+ const normalizedOwnerJid = normalizeJid(ownerJid) || '';
191
+ if (!normalizedSub && !normalizedEmail && !normalizedOwnerJid) return;
192
+
193
+ for (const [token, session] of adminPanelSessionMap.entries()) {
194
+ if (!session || normalizeAdminPanelRole(session.role, 'owner') !== 'moderator') continue;
195
+ const sessionSub = normalizeGoogleSubject(session.googleSub);
196
+ const sessionEmail = normalizeEmail(session.email);
197
+ const sessionOwner = normalizeJid(session.ownerJid) || '';
198
+ if ((normalizedSub && sessionSub === normalizedSub) || (normalizedEmail && sessionEmail === normalizedEmail) || (normalizedOwnerJid && sessionOwner === normalizedOwnerJid)) {
199
+ adminPanelSessionMap.delete(token);
200
+ }
201
+ }
202
+ };
203
+
204
+ const upsertAdminModeratorRecord = async ({ googleSub = '', email = '', ownerJid = '', password = '', adminSession = null }) => {
205
+ const cleanPassword = String(password || '').trim();
206
+ if (cleanPassword.length < ADMIN_MODERATOR_PASSWORD_MIN_LENGTH) {
207
+ const error = new Error(`Senha do moderador deve ter no minimo ${ADMIN_MODERATOR_PASSWORD_MIN_LENGTH} caracteres.`);
208
+ error.statusCode = 400;
209
+ throw error;
210
+ }
211
+
212
+ const knownUser = await resolveKnownGoogleUserForModerator({ googleSub, email, ownerJid });
213
+ if (!knownUser?.google_sub) {
214
+ const error = new Error('Somente usuarios Google logados no site podem virar moderadores.');
215
+ error.statusCode = 400;
216
+ throw error;
217
+ }
218
+
219
+ const existing = await findAdminModeratorByGoogleSub(knownUser.google_sub);
220
+ const passwordHash = hashAdminModeratorPassword(cleanPassword);
221
+ await executeQuery(
222
+ `INSERT INTO ${TABLES.STICKER_WEB_ADMIN_MODERATOR}
223
+ (google_sub, email, owner_jid, name, password_hash, created_by_google_sub, created_by_email, updated_by_google_sub, updated_by_email, revoked_at)
224
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NULL)
225
+ ON DUPLICATE KEY UPDATE
226
+ email = VALUES(email),
227
+ owner_jid = VALUES(owner_jid),
228
+ name = VALUES(name),
229
+ password_hash = VALUES(password_hash),
230
+ updated_by_google_sub = VALUES(updated_by_google_sub),
231
+ updated_by_email = VALUES(updated_by_email),
232
+ revoked_at = NULL`,
233
+ [knownUser.google_sub, knownUser.email, knownUser.owner_jid, knownUser.name || null, passwordHash, normalizeGoogleSubject(adminSession?.googleSub) || null, normalizeEmail(adminSession?.email) || null, normalizeGoogleSubject(adminSession?.googleSub) || null, normalizeEmail(adminSession?.email) || null],
234
+ );
235
+
236
+ pruneModeratorAdminPanelSessions({
237
+ googleSub: knownUser.google_sub,
238
+ email: knownUser.email,
239
+ ownerJid: knownUser.owner_jid,
240
+ });
241
+
242
+ const fresh = await findAdminModeratorByGoogleSub(knownUser.google_sub);
243
+ return {
244
+ created: !existing,
245
+ moderator: mapAdminModeratorRow(fresh),
246
+ };
247
+ };
248
+
249
+ const revokeAdminModeratorRecord = async (googleSub, adminSession = null) => {
250
+ const normalizedSub = normalizeGoogleSubject(googleSub);
251
+ if (!normalizedSub) {
252
+ const error = new Error('google_sub invalido.');
253
+ error.statusCode = 400;
254
+ throw error;
255
+ }
256
+
257
+ await executeQuery(
258
+ `UPDATE ${TABLES.STICKER_WEB_ADMIN_MODERATOR}
259
+ SET revoked_at = COALESCE(revoked_at, UTC_TIMESTAMP()),
260
+ updated_by_google_sub = ?,
261
+ updated_by_email = ?
262
+ WHERE google_sub = ?`,
263
+ [normalizeGoogleSubject(adminSession?.googleSub) || null, normalizeEmail(adminSession?.email) || null, normalizedSub],
264
+ );
265
+
266
+ const fresh = await findAdminModeratorByGoogleSub(normalizedSub);
267
+ if (!fresh) {
268
+ const error = new Error('Moderador nao encontrado.');
269
+ error.statusCode = 404;
270
+ throw error;
271
+ }
272
+
273
+ pruneModeratorAdminPanelSessions({
274
+ googleSub: normalizedSub,
275
+ email: normalizeEmail(fresh.email),
276
+ ownerJid: normalizeJid(fresh.owner_jid) || '',
277
+ });
278
+
279
+ return mapAdminModeratorRow(fresh);
280
+ };
281
+
282
+ const touchAdminModeratorLastLogin = async (moderatorRow, googleSession) => {
283
+ const normalizedSub = normalizeGoogleSubject(moderatorRow?.google_sub || googleSession?.sub);
284
+ if (!normalizedSub) return;
285
+ await executeQuery(
286
+ `UPDATE ${TABLES.STICKER_WEB_ADMIN_MODERATOR}
287
+ SET email = ?,
288
+ owner_jid = ?,
289
+ name = ?,
290
+ last_login_at = UTC_TIMESTAMP(),
291
+ updated_by_google_sub = ?,
292
+ updated_by_email = ?
293
+ WHERE google_sub = ?`,
294
+ [normalizeEmail(googleSession?.email || moderatorRow?.email) || null, normalizeJid(googleSession?.ownerJid || moderatorRow?.owner_jid) || null, sanitizeText(googleSession?.name || moderatorRow?.name || '', 120, { allowEmpty: true }) || null, normalizeGoogleSubject(googleSession?.sub || moderatorRow?.google_sub) || null, normalizeEmail(googleSession?.email || moderatorRow?.email) || null, normalizedSub],
295
+ ).catch(() => {});
296
+ };
297
+
298
+ const pruneExpiredAdminPanelSessions = () => {
299
+ const now = Date.now();
300
+ if (now - adminPanelSessionPruneAt < 30_000) return;
301
+ adminPanelSessionPruneAt = now;
302
+ for (const [token, session] of adminPanelSessionMap.entries()) {
303
+ if (!session || Number(session.expiresAt || 0) <= now) {
304
+ adminPanelSessionMap.delete(token);
305
+ }
306
+ }
307
+ };
308
+
309
+ const getAdminPanelSessionTokenFromRequest = (req) => {
310
+ const direct = getCookieValuesFromRequest(req, ADMIN_PANEL_SESSION_COOKIE_NAME);
311
+ if (direct.length > 0) return direct[0];
312
+ const cookies = parseCookies(req);
313
+ return String(cookies[ADMIN_PANEL_SESSION_COOKIE_NAME] || '').trim();
314
+ };
315
+
316
+ const clearAdminPanelSessionCookie = (req, res) => {
317
+ appendSetCookie(
318
+ res,
319
+ buildCookieString(ADMIN_PANEL_SESSION_COOKIE_NAME, '', req, {
320
+ maxAgeSeconds: 0,
321
+ }),
322
+ );
323
+ // Also clear host-only variant (legacy cookie written without Domain).
324
+ appendSetCookie(
325
+ res,
326
+ buildCookieString(ADMIN_PANEL_SESSION_COOKIE_NAME, '', req, {
327
+ maxAgeSeconds: 0,
328
+ domain: false,
329
+ }),
330
+ );
331
+ };
332
+
333
+ const createAdminPanelSession = (googleSession, { role = 'owner' } = {}) => {
334
+ pruneExpiredAdminPanelSessions();
335
+ const now = Date.now();
336
+ const normalizedRole = normalizeAdminPanelRole(role, 'owner');
337
+ const token = randomUUID();
338
+ const session = {
339
+ token,
340
+ role: normalizedRole,
341
+ googleSub: normalizeGoogleSubject(googleSession?.sub),
342
+ ownerJid: normalizeJid(googleSession?.ownerJid) || '',
343
+ email: normalizeEmail(googleSession?.email),
344
+ name: sanitizeText(googleSession?.name || '', 120, { allowEmpty: true }) || 'Administrador',
345
+ picture: String(googleSession?.picture || '').trim() || '',
346
+ createdAt: now,
347
+ expiresAt: now + ADMIN_PANEL_SESSION_TTL_MS,
348
+ };
349
+ adminPanelSessionMap.set(token, session);
350
+ return session;
351
+ };
352
+
353
+ const resolveAdminPanelSessionFromRequest = (req) => {
354
+ if (!ADMIN_PANEL_ENABLED) return null;
355
+ pruneExpiredAdminPanelSessions();
356
+ const token = getAdminPanelSessionTokenFromRequest(req);
357
+ if (!token) return null;
358
+ const session = adminPanelSessionMap.get(token);
359
+ if (!session) return null;
360
+ if (Number(session.expiresAt || 0) <= Date.now()) {
361
+ adminPanelSessionMap.delete(token);
362
+ return null;
363
+ }
364
+ return session;
365
+ };
366
+
367
+ const mapAdminPanelSessionResponseData = (session) =>
368
+ session
369
+ ? {
370
+ authenticated: true,
371
+ role: normalizeAdminPanelRole(session.role, 'owner'),
372
+ capabilities: {
373
+ can_manage_moderators: normalizeAdminPanelRole(session.role, 'owner') === 'owner',
374
+ },
375
+ user: {
376
+ google_sub: session.googleSub,
377
+ owner_jid: session.ownerJid,
378
+ email: session.email,
379
+ name: session.name,
380
+ picture: session.picture || null,
381
+ },
382
+ expires_at: toIsoOrNull(session.expiresAt),
383
+ }
384
+ : {
385
+ authenticated: false,
386
+ role: null,
387
+ capabilities: {
388
+ can_manage_moderators: false,
389
+ },
390
+ user: null,
391
+ expires_at: null,
392
+ };
393
+
394
+ const isOwnerGoogleSessionAllowed = (googleSession) => {
395
+ if (!ADMIN_PANEL_ENABLED) return false;
396
+ if (!googleSession?.sub || !googleSession?.ownerJid) return false;
397
+ const email = normalizeEmail(googleSession.email);
398
+ return Boolean(email && email === ADMIN_PANEL_EMAIL);
399
+ };
400
+
401
+ const resolveAdminPanelLoginEligibility = async (googleSession) => {
402
+ if (!ADMIN_PANEL_ENABLED || !googleSession?.sub || !googleSession?.ownerJid) {
403
+ return { eligible: false, role: '', moderator: null };
404
+ }
405
+ if (isOwnerGoogleSessionAllowed(googleSession)) {
406
+ return { eligible: true, role: 'owner', moderator: null };
407
+ }
408
+ const moderator = await findActiveAdminModeratorForIdentity({
409
+ googleSub: googleSession.sub,
410
+ email: googleSession.email,
411
+ ownerJid: googleSession.ownerJid,
412
+ });
413
+ if (!moderator) return { eligible: false, role: '', moderator: null };
414
+ return { eligible: true, role: 'moderator', moderator };
415
+ };
416
+
417
+ const requireAdminPanelSession = (req, res) => {
418
+ if (!ADMIN_PANEL_ENABLED) {
419
+ sendJson(req, res, 404, { error: 'Painel admin desabilitado.' });
420
+ return null;
421
+ }
422
+ const session = resolveAdminPanelSessionFromRequest(req);
423
+ if (!session) {
424
+ sendJson(req, res, 401, { error: 'Sessao admin invalida ou expirada.' });
425
+ return null;
426
+ }
427
+ return session;
428
+ };
429
+
430
+ const requireOwnerAdminPanelSession = (req, res) => {
431
+ const session = requireAdminPanelSession(req, res);
432
+ if (!session) return null;
433
+ if (normalizeAdminPanelRole(session.role, 'owner') !== 'owner') {
434
+ sendJson(req, res, 403, { error: 'Somente o dono pode gerenciar moderadores.' });
435
+ return null;
436
+ }
437
+ return session;
438
+ };
439
+ const safeParseJsonObject = (value) => {
440
+ if (!value) return null;
441
+ if (typeof value === 'object') return value;
442
+ try {
443
+ const parsed = JSON.parse(String(value));
444
+ return parsed && typeof parsed === 'object' ? parsed : null;
445
+ } catch {
446
+ return null;
447
+ }
448
+ };
449
+
450
+ const sanitizeAuditActionText = (value, max = 96) =>
451
+ String(value || '')
452
+ .trim()
453
+ .toLowerCase()
454
+ .replace(/[^a-z0-9_:-]/g, '_')
455
+ .slice(0, max);
456
+
457
+ const createAdminActionAuditEvent = async ({ adminSession = null, action = '', targetType = '', targetId = '', status = 'success', details = null } = {}) => {
458
+ const normalizedAction = sanitizeAuditActionText(action, 96);
459
+ if (!normalizedAction) return false;
460
+ const normalizedTargetType = sanitizeAuditActionText(targetType, 64) || null;
461
+ const normalizedStatus = sanitizeAuditActionText(status, 32) || 'success';
462
+ const detailsJson = details && typeof details === 'object' ? JSON.stringify(details) : null;
463
+ const adminRole = normalizeAdminPanelRole(adminSession?.role, 'owner');
464
+ const adminGoogleSub = normalizeGoogleSubject(adminSession?.googleSub) || null;
465
+ const adminEmail = normalizeEmail(adminSession?.email) || null;
466
+ const adminOwnerJid = normalizeJid(adminSession?.ownerJid) || null;
467
+
468
+ try {
469
+ await executeQuery(
470
+ `INSERT INTO ${TABLES.ADMIN_ACTION_AUDIT}
471
+ (
472
+ id,
473
+ admin_role,
474
+ admin_google_sub,
475
+ admin_email,
476
+ admin_owner_jid,
477
+ action,
478
+ target_type,
479
+ target_id,
480
+ status,
481
+ details
482
+ )
483
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
484
+ [randomUUID(), adminRole, adminGoogleSub, adminEmail, adminOwnerJid, normalizedAction, normalizedTargetType, sanitizeText(targetId || '', 255, { allowEmpty: true }) || null, normalizedStatus, detailsJson],
485
+ );
486
+ return true;
487
+ } catch (error) {
488
+ if (error?.code === 'ER_NO_SUCH_TABLE') return false;
489
+ logger.warn('Falha ao registrar auditoria admin.', {
490
+ action: 'admin_audit_insert_failed',
491
+ error: error?.message,
492
+ audit_action: normalizedAction,
493
+ });
494
+ return false;
495
+ }
496
+ };
497
+
498
+ const listAdminAuditLog = async ({ limit = 80 } = {}) => {
499
+ const safeLimit = Math.max(1, Math.min(500, Number(limit || 80)));
500
+ try {
501
+ const rows = await executeQuery(
502
+ `SELECT
503
+ id,
504
+ admin_role,
505
+ admin_google_sub,
506
+ admin_email,
507
+ admin_owner_jid,
508
+ action,
509
+ target_type,
510
+ target_id,
511
+ status,
512
+ details,
513
+ created_at
514
+ FROM ${TABLES.ADMIN_ACTION_AUDIT}
515
+ ORDER BY created_at DESC
516
+ LIMIT ${safeLimit}`,
517
+ );
518
+
519
+ return (Array.isArray(rows) ? rows : []).map((row) => ({
520
+ id: String(row?.id || '').trim(),
521
+ admin_role: normalizeAdminPanelRole(row?.admin_role, 'owner'),
522
+ admin_google_sub: normalizeGoogleSubject(row?.admin_google_sub),
523
+ admin_email: normalizeEmail(row?.admin_email) || null,
524
+ admin_owner_jid: normalizeJid(row?.admin_owner_jid) || null,
525
+ action: String(row?.action || '').trim(),
526
+ target_type: String(row?.target_type || '').trim() || null,
527
+ target_id: String(row?.target_id || '').trim() || null,
528
+ status: String(row?.status || '').trim() || 'success',
529
+ details: safeParseJsonObject(row?.details),
530
+ created_at: toIsoOrNull(row?.created_at),
531
+ }));
532
+ } catch (error) {
533
+ if (error?.code === 'ER_NO_SUCH_TABLE') return [];
534
+ throw error;
535
+ }
536
+ };
537
+
538
+ const listAdminFeatureFlagsDetailed = async ({ limit = 300 } = {}) => {
539
+ const safeLimit = Math.max(1, Math.min(500, Number(limit || 300)));
540
+ try {
541
+ const rows = await executeQuery(
542
+ `SELECT
543
+ flag_name,
544
+ is_enabled,
545
+ rollout_percent,
546
+ description,
547
+ updated_by,
548
+ updated_at
549
+ FROM ${TABLES.FEATURE_FLAG}
550
+ ORDER BY flag_name ASC
551
+ LIMIT ${safeLimit}`,
552
+ );
553
+ return (Array.isArray(rows) ? rows : []).map((row) => ({
554
+ flag_name: sanitizeText(row?.flag_name || '', 120, { allowEmpty: false }) || '',
555
+ is_enabled: Number(row?.is_enabled || 0) === 1,
556
+ rollout_percent: Math.max(0, Math.min(100, Number(row?.rollout_percent || 0))),
557
+ description: sanitizeText(row?.description || '', 255, { allowEmpty: true }) || null,
558
+ updated_by: sanitizeText(row?.updated_by || '', 120, { allowEmpty: true }) || null,
559
+ updated_at: toIsoOrNull(row?.updated_at),
560
+ }));
561
+ } catch (error) {
562
+ if (error?.code === 'ER_NO_SUCH_TABLE') {
563
+ const fallback = await getFeatureFlagsSnapshot().catch(() => []);
564
+ return (Array.isArray(fallback) ? fallback : []).map((entry) => ({
565
+ flag_name: sanitizeText(entry?.flag_name || '', 120, { allowEmpty: false }) || '',
566
+ is_enabled: Boolean(entry?.is_enabled),
567
+ rollout_percent: Math.max(0, Math.min(100, Number(entry?.rollout_percent || 0))),
568
+ description: null,
569
+ updated_by: null,
570
+ updated_at: null,
571
+ }));
572
+ }
573
+ throw error;
574
+ }
575
+ };
576
+
577
+ const upsertAdminFeatureFlagRecord = async ({ adminSession = null, flagName = '', isEnabled = false, rolloutPercent = 100, description = '' } = {}) => {
578
+ const normalizedFlagName = sanitizeAuditActionText(flagName, 120);
579
+ if (!normalizedFlagName) {
580
+ const error = new Error('flag_name invalido.');
581
+ error.statusCode = 400;
582
+ throw error;
583
+ }
584
+ const normalizedRollout = Math.max(0, Math.min(100, Math.floor(Number(rolloutPercent) || 0)));
585
+ const normalizedEnabled = isEnabled ? 1 : 0;
586
+ const normalizedDescription = sanitizeText(description || '', 255, { allowEmpty: true }) || null;
587
+ const updatedBy = normalizeEmail(adminSession?.email) || normalizeGoogleSubject(adminSession?.googleSub) || 'admin';
588
+
589
+ await executeQuery(
590
+ `INSERT INTO ${TABLES.FEATURE_FLAG}
591
+ (flag_name, is_enabled, rollout_percent, description, updated_by)
592
+ VALUES (?, ?, ?, ?, ?)
593
+ ON DUPLICATE KEY UPDATE
594
+ is_enabled = VALUES(is_enabled),
595
+ rollout_percent = VALUES(rollout_percent),
596
+ description = COALESCE(VALUES(description), description),
597
+ updated_by = VALUES(updated_by),
598
+ updated_at = CURRENT_TIMESTAMP`,
599
+ [normalizedFlagName, normalizedEnabled, normalizedRollout, normalizedDescription, sanitizeText(updatedBy, 120, { allowEmpty: true }) || null],
600
+ );
601
+
602
+ await refreshFeatureFlags({ force: true }).catch(() => {});
603
+ const rows = await executeQuery(
604
+ `SELECT flag_name, is_enabled, rollout_percent, description, updated_by, updated_at
605
+ FROM ${TABLES.FEATURE_FLAG}
606
+ WHERE flag_name = ?
607
+ LIMIT 1`,
608
+ [normalizedFlagName],
609
+ );
610
+ const row = Array.isArray(rows) ? rows[0] : null;
611
+ return {
612
+ flag_name: sanitizeText(row?.flag_name || normalizedFlagName, 120, { allowEmpty: false }) || normalizedFlagName,
613
+ is_enabled: Number(row?.is_enabled || 0) === 1,
614
+ rollout_percent: Math.max(0, Math.min(100, Number(row?.rollout_percent ?? normalizedRollout))),
615
+ description: sanitizeText(row?.description || '', 255, { allowEmpty: true }) || null,
616
+ updated_by: sanitizeText(row?.updated_by || '', 120, { allowEmpty: true }) || null,
617
+ updated_at: toIsoOrNull(row?.updated_at) || new Date().toISOString(),
618
+ };
619
+ };
620
+
621
+ const getAdminMessageFlowDailyStats = async () => {
622
+ try {
623
+ const [row] = await executeQuery(
624
+ `SELECT
625
+ COUNT(*) AS messages_today,
626
+ SUM(CASE WHEN processing_result = 'blocked_antilink' THEN 1 ELSE 0 END) AS spam_blocked_today,
627
+ SUM(CASE WHEN processing_result = 'auth_required' THEN 1 ELSE 0 END) AS suspicious_today
628
+ FROM ${TABLES.MESSAGE_ANALYSIS_EVENT}
629
+ WHERE created_at >= UTC_DATE()`,
630
+ );
631
+ return {
632
+ messages_today: Number(row?.messages_today || 0),
633
+ spam_blocked_today: Number(row?.spam_blocked_today || 0),
634
+ suspicious_today: Number(row?.suspicious_today || 0),
635
+ available: true,
636
+ };
637
+ } catch (error) {
638
+ if (error?.code === 'ER_NO_SUCH_TABLE') {
639
+ return {
640
+ messages_today: null,
641
+ spam_blocked_today: null,
642
+ suspicious_today: null,
643
+ available: false,
644
+ };
645
+ }
646
+ throw error;
647
+ }
648
+ };
649
+
650
+ const listRecentModerationEvents = async ({ limit = 40 } = {}) => {
651
+ const safeLimit = Math.max(1, Math.min(200, Number(limit || 40)));
652
+ try {
653
+ const rows = await executeQuery(
654
+ `SELECT
655
+ id,
656
+ message_id,
657
+ chat_id,
658
+ sender_id,
659
+ sender_name,
660
+ processing_result,
661
+ command_name,
662
+ error_code,
663
+ metadata,
664
+ created_at
665
+ FROM ${TABLES.MESSAGE_ANALYSIS_EVENT}
666
+ WHERE processing_result IN ('blocked_antilink', 'auth_required')
667
+ ORDER BY created_at DESC
668
+ LIMIT ${safeLimit}`,
669
+ );
670
+
671
+ return (Array.isArray(rows) ? rows : []).map((row) => {
672
+ const processingResult = String(row?.processing_result || '')
673
+ .trim()
674
+ .toLowerCase();
675
+ const metadata = safeParseJsonObject(row?.metadata);
676
+ const isAntiLink = processingResult === 'blocked_antilink';
677
+ const title = isAntiLink ? 'Anti-link bloqueou mensagem' : 'Tentativa suspeita detectada';
678
+ const severity = isAntiLink ? 'medium' : 'high';
679
+ const sender = sanitizeText(row?.sender_name || row?.sender_id || '', 120, { allowEmpty: true }) || String(row?.sender_id || '').trim() || 'desconhecido';
680
+ const chatId = String(row?.chat_id || '').trim() || 'chat_desconhecido';
681
+ return {
682
+ id: `mae:${row?.id || ''}`,
683
+ event_type: isAntiLink ? 'anti_link' : 'suspicious',
684
+ severity,
685
+ title,
686
+ subtitle: `${sender} em ${chatId}`,
687
+ chat_id: chatId,
688
+ sender_id: String(row?.sender_id || '').trim() || null,
689
+ sender_name: sanitizeText(row?.sender_name || '', 120, { allowEmpty: true }) || null,
690
+ message_id: String(row?.message_id || '').trim() || null,
691
+ processing_result: processingResult,
692
+ command_name: sanitizeText(row?.command_name || '', 64, { allowEmpty: true }) || null,
693
+ error_code: sanitizeText(row?.error_code || '', 96, { allowEmpty: true }) || null,
694
+ metadata,
695
+ created_at: toIsoOrNull(row?.created_at),
696
+ };
697
+ });
698
+ } catch (error) {
699
+ if (error?.code === 'ER_NO_SUCH_TABLE') return [];
700
+ throw error;
701
+ }
702
+ };
703
+
704
+ const buildModerationQueueSnapshot = async ({ limit = 50 } = {}) => {
705
+ const [analysisEvents, bans] = await Promise.all([listRecentModerationEvents({ limit: Math.max(10, limit) }), listAdminBans({ activeOnly: false, limit: Math.max(10, Math.floor(limit / 2)) })]);
706
+
707
+ const banEvents = (Array.isArray(bans) ? bans : []).map((ban) => ({
708
+ id: `ban:${ban?.id || ''}`,
709
+ event_type: 'ban',
710
+ severity: ban?.revoked_at ? 'low' : 'critical',
711
+ title: ban?.revoked_at ? 'Ban revogado' : 'Conta bloqueada',
712
+ subtitle: sanitizeText(ban?.email || ban?.owner_jid || ban?.google_sub || '', 160, { allowEmpty: true }) || 'identidade indisponivel',
713
+ ban_id: String(ban?.id || '').trim(),
714
+ reason: sanitizeText(ban?.reason || '', 255, { allowEmpty: true }) || null,
715
+ created_at: toIsoOrNull(ban?.created_at),
716
+ revoked_at: toIsoOrNull(ban?.revoked_at),
717
+ metadata: {
718
+ google_sub: ban?.google_sub || null,
719
+ email: ban?.email || null,
720
+ owner_jid: ban?.owner_jid || null,
721
+ },
722
+ }));
723
+
724
+ const combined = [...(Array.isArray(analysisEvents) ? analysisEvents : []), ...banEvents];
725
+ combined.sort((left, right) => {
726
+ const leftTs = Date.parse(String(left?.created_at || left?.revoked_at || 0)) || 0;
727
+ const rightTs = Date.parse(String(right?.created_at || right?.revoked_at || 0)) || 0;
728
+ return rightTs - leftTs;
729
+ });
730
+ return combined.slice(0, Math.max(1, Math.min(200, Number(limit || 50))));
731
+ };
732
+
733
+ const buildAdminSystemHealthSnapshot = ({ systemSummary = null, systemMeta = null } = {}) => {
734
+ const hostCpu = Number(systemSummary?.host?.cpu_percent);
735
+ const hostRam = Number(systemSummary?.host?.memory_percent);
736
+ const latencyP95 = Number(systemSummary?.observability?.http_latency_p95_ms);
737
+ const queuePending = Number(systemSummary?.observability?.queue_peak);
738
+ const hasMetricsError = Boolean(systemMeta?.metrics_error);
739
+ const hasPlatformError = Boolean(systemMeta?.platform_error);
740
+ const dbStatus = hasPlatformError ? 'degraded' : hasMetricsError ? 'unknown' : 'ok';
741
+
742
+ return {
743
+ cpu_percent: Number.isFinite(hostCpu) ? hostCpu : null,
744
+ ram_percent: Number.isFinite(hostRam) ? hostRam : null,
745
+ http_latency_p95_ms: Number.isFinite(latencyP95) ? latencyP95 : null,
746
+ queue_pending: Number.isFinite(queuePending) ? queuePending : null,
747
+ db_status: dbStatus,
748
+ db_total_queries: Number(systemSummary?.observability?.db_total ?? 0) || 0,
749
+ db_slow_queries: Number(systemSummary?.observability?.db_slow ?? 0) || 0,
750
+ bot_status: String(systemSummary?.bot?.connection_status || '').trim() || 'unknown',
751
+ updated_at: toIsoOrNull(systemSummary?.updated_at),
752
+ };
753
+ };
754
+
755
+ const buildAdminAlertSnapshot = ({ dashboardQuick = null, systemHealth = null, systemSummary = null, systemMeta = null } = {}) => {
756
+ const alerts = [];
757
+ const updatedAt = toIsoOrNull(systemSummary?.updated_at) || new Date().toISOString();
758
+ const pushAlert = (severity, code, title, message) => {
759
+ alerts.push({
760
+ id: `${code}:${severity}`,
761
+ severity,
762
+ code,
763
+ title,
764
+ message,
765
+ created_at: updatedAt,
766
+ });
767
+ };
768
+
769
+ const botStatus = String(systemSummary?.bot?.connection_status || '').toLowerCase();
770
+ if (botStatus && botStatus !== 'online') {
771
+ pushAlert('critical', 'bot_offline', 'Bot fora do ar', `Status atual: ${botStatus}.`);
772
+ }
773
+
774
+ if (Number.isFinite(systemHealth?.cpu_percent) && systemHealth.cpu_percent >= 90) {
775
+ pushAlert('high', 'cpu_high', 'CPU alta', `Uso de CPU em ${systemHealth.cpu_percent.toFixed(1)}%.`);
776
+ }
777
+ if (Number.isFinite(systemHealth?.ram_percent) && systemHealth.ram_percent >= 90) {
778
+ pushAlert('high', 'ram_high', 'RAM alta', `Uso de RAM em ${systemHealth.ram_percent.toFixed(1)}%.`);
779
+ }
780
+ if (Number.isFinite(systemHealth?.queue_pending) && systemHealth.queue_pending >= 100) {
781
+ pushAlert('medium', 'queue_high', 'Fila pendente alta', `Backlog detectado (${Math.round(systemHealth.queue_pending)}).`);
782
+ }
783
+ if (Number.isFinite(dashboardQuick?.errors_5xx) && dashboardQuick.errors_5xx > 0) {
784
+ pushAlert('medium', 'http_5xx', 'Erros HTTP 5xx detectados', `${Math.round(dashboardQuick.errors_5xx)} eventos 5xx desde o boot de métricas.`);
785
+ }
786
+ if (systemMeta?.platform_error) {
787
+ pushAlert('high', 'db_platform_error', 'Erro de banco/plataforma', String(systemMeta.platform_error).slice(0, 200));
788
+ }
789
+ if (systemMeta?.metrics_error) {
790
+ pushAlert('low', 'metrics_unavailable', 'Métricas indisponíveis', String(systemMeta.metrics_error).slice(0, 200));
791
+ }
792
+
793
+ return alerts;
794
+ };
795
+
796
+ const listAdminActiveGoogleWebSessions = async ({ limit = 200 } = {}) => {
797
+ const safeLimit = Math.max(1, Math.min(500, Number(limit || 200)));
798
+ const rows = await executeQuery(
799
+ `SELECT session_token, google_sub, owner_jid, owner_phone, email, name, picture_url, created_at, last_seen_at, expires_at
800
+ FROM ${TABLES.STICKER_WEB_GOOGLE_SESSION}
801
+ WHERE revoked_at IS NULL
802
+ AND expires_at > UTC_TIMESTAMP()
803
+ ORDER BY COALESCE(last_seen_at, created_at) DESC
804
+ LIMIT ${safeLimit}`,
805
+ );
806
+ return (Array.isArray(rows) ? rows : []).map((row) => ({
807
+ session_token: String(row.session_token || '').trim(),
808
+ google_sub: normalizeGoogleSubject(row.google_sub),
809
+ owner_jid: normalizeJid(row.owner_jid) || null,
810
+ owner_phone: toWhatsAppPhoneDigits(row.owner_phone || row.owner_jid) || null,
811
+ email: normalizeEmail(row.email) || null,
812
+ name: sanitizeText(row.name || '', 120, { allowEmpty: true }) || null,
813
+ picture: String(row.picture_url || '').trim() || null,
814
+ created_at: toIsoOrNull(row.created_at),
815
+ last_seen_at: toIsoOrNull(row.last_seen_at),
816
+ expires_at: toIsoOrNull(row.expires_at),
817
+ }));
818
+ };
819
+
820
+ const listAdminKnownGoogleUsers = async ({ limit = 200 } = {}) => {
821
+ const safeLimit = Math.max(1, Math.min(500, Number(limit || 200)));
822
+ const rows = await executeQuery(
823
+ `SELECT google_sub, owner_jid, owner_phone, email, name, picture_url, created_at, updated_at, last_login_at, last_seen_at
824
+ FROM ${TABLES.STICKER_WEB_GOOGLE_USER}
825
+ ORDER BY COALESCE(last_seen_at, last_login_at, updated_at, created_at) DESC
826
+ LIMIT ${safeLimit}`,
827
+ );
828
+ return (Array.isArray(rows) ? rows : []).map((row) => ({
829
+ google_sub: normalizeGoogleSubject(row.google_sub),
830
+ owner_jid: normalizeJid(row.owner_jid) || null,
831
+ owner_phone: toWhatsAppPhoneDigits(row.owner_phone || row.owner_jid) || null,
832
+ email: normalizeEmail(row.email) || null,
833
+ name: sanitizeText(row.name || '', 120, { allowEmpty: true }) || null,
834
+ picture: String(row.picture_url || '').trim() || null,
835
+ created_at: toIsoOrNull(row.created_at),
836
+ updated_at: toIsoOrNull(row.updated_at),
837
+ last_login_at: toIsoOrNull(row.last_login_at),
838
+ last_seen_at: toIsoOrNull(row.last_seen_at),
839
+ }));
840
+ };
841
+
842
+ const getWebVisitSummary = async ({ rangeDays = 7, topPathsLimit = 10 } = {}) => {
843
+ const safeRangeDays = Math.max(1, Math.min(90, Number(rangeDays || 7)));
844
+ const safeTopPathsLimit = Math.max(1, Math.min(30, Number(topPathsLimit || 10)));
845
+
846
+ try {
847
+ const [countersRows, topPathsRows] = await Promise.all([
848
+ executeQuery(
849
+ `SELECT
850
+ COUNT(*) AS total_events,
851
+ SUM(CASE WHEN created_at >= (UTC_TIMESTAMP() - INTERVAL 1 DAY) THEN 1 ELSE 0 END) AS events_24h,
852
+ SUM(CASE WHEN created_at >= (UTC_TIMESTAMP() - INTERVAL ${safeRangeDays} DAY) THEN 1 ELSE 0 END) AS events_range,
853
+ COUNT(DISTINCT CASE WHEN created_at >= (UTC_TIMESTAMP() - INTERVAL ${safeRangeDays} DAY) THEN visitor_key END) AS unique_visitors_range,
854
+ COUNT(DISTINCT CASE WHEN created_at >= (UTC_TIMESTAMP() - INTERVAL ${safeRangeDays} DAY) THEN session_key END) AS unique_sessions_range
855
+ FROM ${TABLES.WEB_VISIT_EVENT}`,
856
+ ),
857
+ executeQuery(
858
+ `SELECT page_path, COUNT(*) AS total
859
+ FROM ${TABLES.WEB_VISIT_EVENT}
860
+ WHERE created_at >= (UTC_TIMESTAMP() - INTERVAL ${safeRangeDays} DAY)
861
+ GROUP BY page_path
862
+ ORDER BY total DESC
863
+ LIMIT ${safeTopPathsLimit}`,
864
+ ),
865
+ ]);
866
+
867
+ const counters = Array.isArray(countersRows) ? countersRows[0] || {} : {};
868
+ const topPaths = (Array.isArray(topPathsRows) ? topPathsRows : []).map((row) => ({
869
+ page_path: normalizeVisitPath(row?.page_path || '/'),
870
+ total: Number(row?.total || 0),
871
+ }));
872
+
873
+ return {
874
+ range_days: safeRangeDays,
875
+ total_events: Number(counters?.total_events || 0),
876
+ events_24h: Number(counters?.events_24h || 0),
877
+ events_range: Number(counters?.events_range || 0),
878
+ unique_visitors_range: Number(counters?.unique_visitors_range || 0),
879
+ unique_sessions_range: Number(counters?.unique_sessions_range || 0),
880
+ top_paths: topPaths,
881
+ };
882
+ } catch (error) {
883
+ if (error?.code === 'ER_NO_SUCH_TABLE') return null;
884
+ throw error;
885
+ }
886
+ };
887
+
888
+ const listAdminPacks = async (url) => {
889
+ const q = sanitizeText(url?.searchParams?.get('q') || '', 120, { allowEmpty: true }) || '';
890
+ const owner = normalizeJid(url?.searchParams?.get('owner_jid') || '') || '';
891
+ const limit = Math.max(1, Math.min(200, Number(url?.searchParams?.get('limit') || 50)));
892
+ const params = [];
893
+ const where = ['p.deleted_at IS NULL'];
894
+ if (q) {
895
+ where.push('(p.pack_key LIKE ? OR p.name LIKE ? OR p.publisher LIKE ? OR p.owner_jid LIKE ?)');
896
+ const like = `%${q}%`;
897
+ params.push(like, like, like, like);
898
+ }
899
+ if (owner) {
900
+ where.push('p.owner_jid = ?');
901
+ params.push(owner);
902
+ }
903
+ const rows = await executeQuery(
904
+ `SELECT
905
+ p.id,
906
+ p.pack_key,
907
+ p.owner_jid,
908
+ p.name,
909
+ p.publisher,
910
+ p.visibility,
911
+ p.status,
912
+ p.pack_status,
913
+ p.is_auto_pack,
914
+ p.pack_theme_key,
915
+ p.pack_volume,
916
+ p.created_at,
917
+ p.updated_at,
918
+ p.cover_sticker_id,
919
+ (SELECT COUNT(*) FROM ${TABLES.STICKER_PACK_ITEM} i WHERE i.pack_id = p.id) AS stickers_count,
920
+ COALESCE(e.open_count, 0) AS open_count,
921
+ COALESCE(e.like_count, 0) AS like_count,
922
+ COALESCE(e.dislike_count, 0) AS dislike_count
923
+ FROM ${TABLES.STICKER_PACK} p
924
+ LEFT JOIN ${TABLES.STICKER_PACK_ENGAGEMENT} e ON e.pack_id = p.id
925
+ WHERE ${where.join(' AND ')}
926
+ ORDER BY p.updated_at DESC
927
+ LIMIT ${limit}`,
928
+ params,
929
+ );
930
+ return (Array.isArray(rows) ? rows : []).map((row) => ({
931
+ id: String(row.id || ''),
932
+ pack_key: String(row.pack_key || ''),
933
+ owner_jid: normalizeJid(row.owner_jid) || null,
934
+ name: String(row.name || ''),
935
+ publisher: String(row.publisher || ''),
936
+ visibility: String(row.visibility || ''),
937
+ status: String(row.status || ''),
938
+ pack_status: String(row.pack_status || 'ready'),
939
+ is_auto_pack: Boolean(Number(row.is_auto_pack || 0)),
940
+ pack_theme_key: String(row.pack_theme_key || '').trim() || null,
941
+ pack_volume: Number.isFinite(Number(row.pack_volume)) ? Number(row.pack_volume) : null,
942
+ created_at: toIsoOrNull(row.created_at),
943
+ updated_at: toIsoOrNull(row.updated_at),
944
+ cover_sticker_id: String(row.cover_sticker_id || '').trim() || null,
945
+ stickers_count: Number(row.stickers_count || 0),
946
+ open_count: Number(row.open_count || 0),
947
+ like_count: Number(row.like_count || 0),
948
+ dislike_count: Number(row.dislike_count || 0),
949
+ web_url: `${STICKER_WEB_PATH}/${encodeURIComponent(String(row.pack_key || ''))}`,
950
+ }));
951
+ };
952
+
953
+ const buildAdminOverviewPayload = async ({ adminSession = null } = {}) => {
954
+ const [marketplaceStats, activeSessions, knownUsers, bans, packsCountRows, stickersCountRows, recentPacks, visitSummary, systemSummaryPayload, messageFlowDaily, moderationQueue, auditLog, featureFlags] = await Promise.all([getMarketplaceGlobalStatsCached().catch(() => null), listAdminActiveGoogleWebSessions({ limit: 80 }), listAdminKnownGoogleUsers({ limit: 120 }), listAdminBans({ activeOnly: true, limit: 120 }), executeQuery(`SELECT COUNT(*) AS total FROM ${TABLES.STICKER_PACK} WHERE deleted_at IS NULL`), executeQuery(`SELECT COUNT(*) AS total FROM ${TABLES.STICKER_ASSET}`), listAdminPacks({ searchParams: new URLSearchParams([['limit', '30']]) }), getWebVisitSummary({ rangeDays: 7, topPathsLimit: 10 }).catch(() => null), getSystemSummaryCached().catch(() => null), getAdminMessageFlowDailyStats().catch(() => ({ messages_today: null, spam_blocked_today: null, suspicious_today: null, available: false })), buildModerationQueueSnapshot({ limit: 80 }).catch(() => []), listAdminAuditLog({ limit: 120 }).catch(() => []), listAdminFeatureFlagsDetailed({ limit: 300 }).catch(() => [])]);
955
+
956
+ const systemSummary = systemSummaryPayload?.data || null;
957
+ const systemMeta = systemSummaryPayload?.meta || null;
958
+ const botsOnline = systemSummary?.bot?.connected ? 1 : 0;
959
+ const errors5xx = Number(systemSummary?.observability?.http_5xx_total ?? 0);
960
+ const dashboardQuick = {
961
+ bots_online: botsOnline,
962
+ messages_today: Number(messageFlowDaily?.messages_today ?? 0),
963
+ spam_blocked_today: Number(messageFlowDaily?.spam_blocked_today ?? 0),
964
+ uptime: String(systemSummary?.process?.uptime || '').trim() || 'n/d',
965
+ errors_5xx: Number.isFinite(errors5xx) ? Math.max(0, errors5xx) : 0,
966
+ };
967
+ const systemHealth = buildAdminSystemHealthSnapshot({ systemSummary, systemMeta });
968
+ const alerts = buildAdminAlertSnapshot({ dashboardQuick, systemHealth, systemSummary, systemMeta });
969
+
970
+ return {
971
+ admin_session: mapAdminPanelSessionResponseData(adminSession),
972
+ marketplace_stats: marketplaceStats,
973
+ counters: {
974
+ total_packs_any_status: Number(packsCountRows?.[0]?.total || 0),
975
+ total_stickers_any_status: Number(stickersCountRows?.[0]?.total || 0),
976
+ active_google_sessions: Number(activeSessions.length || 0),
977
+ known_google_users: Number(knownUsers.length || 0),
978
+ active_bans: Number(bans.length || 0),
979
+ visit_events_24h: Number(visitSummary?.events_24h || 0),
980
+ visit_events_7d: Number(visitSummary?.events_range || 0),
981
+ unique_visitors_7d: Number(visitSummary?.unique_visitors_range || 0),
982
+ },
983
+ dashboard_quick: dashboardQuick,
984
+ moderation_queue: moderationQueue,
985
+ users_sessions: {
986
+ active_sessions: activeSessions,
987
+ users: knownUsers,
988
+ blocked_accounts: bans,
989
+ },
990
+ system_health: systemHealth,
991
+ audit_log: auditLog,
992
+ feature_flags: featureFlags,
993
+ alerts,
994
+ operational_shortcuts: [
995
+ { action: 'restart_worker', label: 'Reiniciar worker', description: 'Destrava filas em processamento e recoloca em pending.' },
996
+ { action: 'clear_cache', label: 'Limpar cache', description: 'Invalida caches internos de catálogo, ranking e resumo.' },
997
+ { action: 'reprocess_jobs', label: 'Reprocessar jobs', description: 'Agenda ciclos de classificação/curadoria no worker.' },
998
+ ],
999
+ active_sessions: activeSessions,
1000
+ users: knownUsers,
1001
+ bans,
1002
+ recent_packs: recentPacks,
1003
+ visit_metrics: visitSummary,
1004
+ system_summary: systemSummary,
1005
+ system_meta: systemMeta,
1006
+ message_flow_daily: messageFlowDaily,
1007
+ updated_at: new Date().toISOString(),
1008
+ };
1009
+ };
1010
+
1011
+ const findAdminPackContextByKey = async (rawPackKey) => {
1012
+ const packKey = sanitizeText(rawPackKey, 160, { allowEmpty: false });
1013
+ if (!packKey) return null;
1014
+ const basePack = await findStickerPackByPackKey(packKey);
1015
+ if (!basePack) return null;
1016
+ const ownerJid = normalizeJid(basePack.owner_jid) || '';
1017
+ if (!ownerJid) return null;
1018
+ const fullPack = await stickerPackService.getPackInfo({ ownerJid, identifier: basePack.pack_key });
1019
+ return { basePack, fullPack, ownerJid, packKey: basePack.pack_key };
1020
+ };
1021
+
1022
+ const handleAdminPanelSessionRequest = async (req, res) => {
1023
+ if (!ADMIN_PANEL_ENABLED) {
1024
+ sendJson(req, res, 404, { error: 'Painel admin desabilitado.' });
1025
+ return;
1026
+ }
1027
+
1028
+ if (req.method === 'GET' || req.method === 'HEAD') {
1029
+ const googleSession = await resolveGoogleWebSessionFromRequest(req);
1030
+ const adminSession = resolveAdminPanelSessionFromRequest(req);
1031
+ const eligibility = await resolveAdminPanelLoginEligibility(googleSession);
1032
+ sendJson(req, res, 200, {
1033
+ data: {
1034
+ google: mapGoogleSessionResponseData(googleSession),
1035
+ eligible_google_login: Boolean(eligibility.eligible),
1036
+ eligible_role: eligibility.role || null,
1037
+ session: mapAdminPanelSessionResponseData(adminSession),
1038
+ },
1039
+ });
1040
+ return;
1041
+ }
1042
+
1043
+ if (req.method === 'DELETE') {
1044
+ const token = getAdminPanelSessionTokenFromRequest(req);
1045
+ const adminSession = resolveAdminPanelSessionFromRequest(req);
1046
+ if (token) adminPanelSessionMap.delete(token);
1047
+ clearAdminPanelSessionCookie(req, res);
1048
+ await createAdminActionAuditEvent({
1049
+ adminSession,
1050
+ action: 'admin_session_logout',
1051
+ targetType: 'admin_session',
1052
+ targetId: token || 'cookie_clear',
1053
+ });
1054
+ sendJson(req, res, 200, { data: { cleared: true } });
1055
+ return;
1056
+ }
1057
+
1058
+ if (req.method !== 'POST') {
1059
+ sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
1060
+ return;
1061
+ }
1062
+
1063
+ let payload = {};
1064
+ try {
1065
+ payload = await readJsonBody(req);
1066
+ } catch (error) {
1067
+ sendJson(req, res, Number(error?.statusCode || 400), { error: error?.message || 'Body inválido.' });
1068
+ return;
1069
+ }
1070
+
1071
+ const googleSession = await resolveGoogleWebSessionFromRequest(req);
1072
+ const eligibility = await resolveAdminPanelLoginEligibility(googleSession);
1073
+ if (!eligibility.eligible) {
1074
+ sendJson(req, res, 403, { error: 'Conta Google sem permissao para o painel admin.' });
1075
+ return;
1076
+ }
1077
+ const password = String(payload?.password || '').trim();
1078
+ let sessionRole = 'owner';
1079
+ if (eligibility.role === 'owner') {
1080
+ if (!password || !constantTimeStringEqual(password, ADMIN_PANEL_PASSWORD)) {
1081
+ sendJson(req, res, 401, { error: 'Senha do painel admin invalida.' });
1082
+ return;
1083
+ }
1084
+ sessionRole = 'owner';
1085
+ } else if (eligibility.role === 'moderator') {
1086
+ const moderatorHash = String(eligibility?.moderator?.password_hash || '').trim();
1087
+ if (!password || !verifyAdminModeratorPassword(password, moderatorHash)) {
1088
+ sendJson(req, res, 401, { error: 'Senha do moderador invalida.' });
1089
+ return;
1090
+ }
1091
+ sessionRole = 'moderator';
1092
+ await touchAdminModeratorLastLogin(eligibility.moderator, googleSession).catch(() => {});
1093
+ } else {
1094
+ sendJson(req, res, 403, { error: 'Conta Google sem permissao para o painel admin.' });
1095
+ return;
1096
+ }
1097
+
1098
+ const session = createAdminPanelSession(googleSession, { role: sessionRole });
1099
+ appendSetCookie(
1100
+ res,
1101
+ buildCookieString(ADMIN_PANEL_SESSION_COOKIE_NAME, session.token, req, {
1102
+ maxAgeSeconds: Math.floor(ADMIN_PANEL_SESSION_TTL_MS / 1000),
1103
+ }),
1104
+ );
1105
+ sendJson(req, res, 200, {
1106
+ data: {
1107
+ google: mapGoogleSessionResponseData(googleSession),
1108
+ eligible_google_login: true,
1109
+ eligible_role: sessionRole,
1110
+ session: mapAdminPanelSessionResponseData(session),
1111
+ },
1112
+ });
1113
+ await createAdminActionAuditEvent({
1114
+ adminSession: session,
1115
+ action: 'admin_session_login',
1116
+ targetType: 'admin_session',
1117
+ targetId: session.token,
1118
+ details: { role: sessionRole },
1119
+ });
1120
+ };
1121
+
1122
+ const handleAdminOverviewRequest = async (req, res) => {
1123
+ const adminSession = requireAdminPanelSession(req, res);
1124
+ if (!adminSession) return;
1125
+ if (!['GET', 'HEAD'].includes(req.method || '')) {
1126
+ sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
1127
+ return;
1128
+ }
1129
+
1130
+ const overview = await buildAdminOverviewPayload({ adminSession });
1131
+ sendJson(req, res, 200, { data: overview });
1132
+ };
1133
+
1134
+ const handleAdminUsersRequest = async (req, res, url) => {
1135
+ const adminSession = requireAdminPanelSession(req, res);
1136
+ if (!adminSession) return;
1137
+ if (!['GET', 'HEAD'].includes(req.method || '')) {
1138
+ sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
1139
+ return;
1140
+ }
1141
+ const limit = Math.max(1, Math.min(500, Number(url?.searchParams?.get('limit') || 200)));
1142
+ const [activeSessions, users] = await Promise.all([listAdminActiveGoogleWebSessions({ limit }), listAdminKnownGoogleUsers({ limit })]);
1143
+ sendJson(req, res, 200, { data: { active_sessions: activeSessions, users } });
1144
+ };
1145
+
1146
+ const handleAdminForceLogoutRequest = async (req, res) => {
1147
+ const adminSession = requireAdminPanelSession(req, res);
1148
+ if (!adminSession) return;
1149
+ if (req.method !== 'POST') {
1150
+ sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
1151
+ return;
1152
+ }
1153
+
1154
+ let payload = {};
1155
+ try {
1156
+ payload = await readJsonBody(req);
1157
+ } catch (error) {
1158
+ sendJson(req, res, Number(error?.statusCode || 400), { error: error?.message || 'Body inválido.' });
1159
+ return;
1160
+ }
1161
+
1162
+ let googleSub = normalizeGoogleSubject(payload?.google_sub || '');
1163
+ let email = normalizeEmail(payload?.email || '');
1164
+ let ownerJid = normalizeJid(payload?.owner_jid || '') || '';
1165
+ const sessionToken = sanitizeText(payload?.session_token || '', 36, { allowEmpty: true }) || '';
1166
+
1167
+ if (sessionToken) {
1168
+ const rows = await executeQuery(
1169
+ `SELECT google_sub, email, owner_jid
1170
+ FROM ${TABLES.STICKER_WEB_GOOGLE_SESSION}
1171
+ WHERE session_token = ?
1172
+ LIMIT 1`,
1173
+ [sessionToken],
1174
+ ).catch(() => []);
1175
+ const row = Array.isArray(rows) ? rows[0] : null;
1176
+ if (row) {
1177
+ googleSub = normalizeGoogleSubject(row.google_sub || googleSub);
1178
+ email = normalizeEmail(row.email || email);
1179
+ ownerJid = normalizeJid(row.owner_jid || ownerJid) || ownerJid;
1180
+ }
1181
+ }
1182
+
1183
+ if (!googleSub && !email && !ownerJid) {
1184
+ sendJson(req, res, 400, { error: 'Informe session_token, google_sub, email ou owner_jid.' });
1185
+ return;
1186
+ }
1187
+
1188
+ const removed = await revokeGoogleWebSessionsByIdentity({
1189
+ googleSub,
1190
+ email,
1191
+ ownerJid,
1192
+ }).catch(() => 0);
1193
+
1194
+ await createAdminActionAuditEvent({
1195
+ adminSession,
1196
+ action: 'force_logout',
1197
+ targetType: 'google_web_session',
1198
+ targetId: sessionToken || googleSub || email || ownerJid,
1199
+ details: { removed_sessions: Number(removed || 0), google_sub: googleSub || null, email: email || null, owner_jid: ownerJid || null },
1200
+ });
1201
+
1202
+ sendJson(req, res, 200, {
1203
+ data: {
1204
+ removed_sessions: Number(removed || 0),
1205
+ target: {
1206
+ session_token: sessionToken || null,
1207
+ google_sub: googleSub || null,
1208
+ email: email || null,
1209
+ owner_jid: ownerJid || null,
1210
+ },
1211
+ },
1212
+ });
1213
+ };
1214
+
1215
+ const handleAdminFeatureFlagsRequest = async (req, res) => {
1216
+ const adminSession = requireAdminPanelSession(req, res);
1217
+ if (!adminSession) return;
1218
+
1219
+ if (req.method === 'GET' || req.method === 'HEAD') {
1220
+ const flags = await listAdminFeatureFlagsDetailed({ limit: 400 }).catch(() => []);
1221
+ sendJson(req, res, 200, { data: { flags } });
1222
+ return;
1223
+ }
1224
+
1225
+ if (req.method !== 'POST') {
1226
+ sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
1227
+ return;
1228
+ }
1229
+
1230
+ let payload = {};
1231
+ try {
1232
+ payload = await readJsonBody(req);
1233
+ } catch (error) {
1234
+ sendJson(req, res, Number(error?.statusCode || 400), { error: error?.message || 'Body inválido.' });
1235
+ return;
1236
+ }
1237
+
1238
+ try {
1239
+ const flag = await upsertAdminFeatureFlagRecord({
1240
+ adminSession,
1241
+ flagName: payload?.flag_name,
1242
+ isEnabled: Boolean(payload?.is_enabled),
1243
+ rolloutPercent: payload?.rollout_percent,
1244
+ description: payload?.description,
1245
+ });
1246
+ await createAdminActionAuditEvent({
1247
+ adminSession,
1248
+ action: 'feature_flag_update',
1249
+ targetType: 'feature_flag',
1250
+ targetId: flag.flag_name,
1251
+ details: {
1252
+ is_enabled: flag.is_enabled,
1253
+ rollout_percent: flag.rollout_percent,
1254
+ },
1255
+ });
1256
+ sendJson(req, res, 200, { data: { flag } });
1257
+ } catch (error) {
1258
+ sendJson(req, res, Number(error?.statusCode || 400), { error: error?.message || 'Falha ao atualizar feature flag.' });
1259
+ }
1260
+ };
1261
+
1262
+ const handleAdminOpsActionRequest = async (req, res) => {
1263
+ const adminSession = requireAdminPanelSession(req, res);
1264
+ if (!adminSession) return;
1265
+ if (req.method !== 'POST') {
1266
+ sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
1267
+ return;
1268
+ }
1269
+
1270
+ let payload = {};
1271
+ try {
1272
+ payload = await readJsonBody(req);
1273
+ } catch (error) {
1274
+ sendJson(req, res, Number(error?.statusCode || 400), { error: error?.message || 'Body inválido.' });
1275
+ return;
1276
+ }
1277
+
1278
+ const action = sanitizeAuditActionText(payload?.action || '', 64);
1279
+ if (!action) {
1280
+ sendJson(req, res, 400, { error: 'Informe a ação operacional.' });
1281
+ return;
1282
+ }
1283
+
1284
+ try {
1285
+ if (action === 'clear_cache') {
1286
+ invalidateStickerCatalogDerivedCaches();
1287
+ await createAdminActionAuditEvent({
1288
+ adminSession,
1289
+ action: 'ops_clear_cache',
1290
+ targetType: 'cache',
1291
+ targetId: 'global',
1292
+ });
1293
+ sendJson(req, res, 200, { data: { action, success: true, message: 'Caches internos invalidados com sucesso.', updated_at: new Date().toISOString() } });
1294
+ return;
1295
+ }
1296
+
1297
+ if (action === 'restart_worker') {
1298
+ const [tasksResult, reprocessResult] = await Promise.all([
1299
+ executeQuery(
1300
+ `UPDATE ${TABLES.STICKER_WORKER_TASK_QUEUE}
1301
+ SET status = 'pending',
1302
+ worker_token = NULL,
1303
+ locked_at = NULL,
1304
+ updated_at = CURRENT_TIMESTAMP
1305
+ WHERE status = 'processing'`,
1306
+ ).catch(() => ({ affectedRows: 0 })),
1307
+ executeQuery(
1308
+ `UPDATE ${TABLES.STICKER_ASSET_REPROCESS_QUEUE}
1309
+ SET status = 'pending',
1310
+ worker_token = NULL,
1311
+ locked_at = NULL,
1312
+ updated_at = CURRENT_TIMESTAMP
1313
+ WHERE status = 'processing'`,
1314
+ ).catch(() => ({ affectedRows: 0 })),
1315
+ ]);
1316
+
1317
+ const released = Number(tasksResult?.affectedRows || 0) + Number(reprocessResult?.affectedRows || 0);
1318
+ await createAdminActionAuditEvent({
1319
+ adminSession,
1320
+ action: 'ops_restart_worker',
1321
+ targetType: 'worker',
1322
+ targetId: 'queues',
1323
+ details: {
1324
+ released_tasks: released,
1325
+ task_queue: Number(tasksResult?.affectedRows || 0),
1326
+ reprocess_queue: Number(reprocessResult?.affectedRows || 0),
1327
+ },
1328
+ });
1329
+ sendJson(req, res, 200, {
1330
+ data: {
1331
+ action,
1332
+ success: true,
1333
+ released_processing_items: released,
1334
+ message: released > 0 ? 'Itens em processamento foram recolocados em pending.' : 'Nenhum item travado encontrado nas filas.',
1335
+ updated_at: new Date().toISOString(),
1336
+ },
1337
+ });
1338
+ return;
1339
+ }
1340
+
1341
+ if (action === 'reprocess_jobs') {
1342
+ const payloadJson = JSON.stringify({
1343
+ source: 'admin_panel',
1344
+ requested_by: normalizeEmail(adminSession?.email) || normalizeGoogleSubject(adminSession?.googleSub) || 'admin',
1345
+ requested_at: new Date().toISOString(),
1346
+ });
1347
+ await executeQuery(
1348
+ `INSERT INTO ${TABLES.STICKER_WORKER_TASK_QUEUE}
1349
+ (task_type, payload, priority, scheduled_at, status, max_attempts)
1350
+ VALUES
1351
+ ('classification_cycle', ?, 10, UTC_TIMESTAMP(), 'pending', 5),
1352
+ ('curation_cycle', ?, 12, UTC_TIMESTAMP(), 'pending', 5)`,
1353
+ [payloadJson, payloadJson],
1354
+ );
1355
+ await createAdminActionAuditEvent({
1356
+ adminSession,
1357
+ action: 'ops_reprocess_jobs',
1358
+ targetType: 'worker',
1359
+ targetId: 'classification_cycle,curation_cycle',
1360
+ });
1361
+ sendJson(req, res, 200, {
1362
+ data: {
1363
+ action,
1364
+ success: true,
1365
+ enqueued_tasks: 2,
1366
+ message: 'Ciclos de classificação e curadoria foram agendados.',
1367
+ updated_at: new Date().toISOString(),
1368
+ },
1369
+ });
1370
+ return;
1371
+ }
1372
+
1373
+ sendJson(req, res, 400, { error: 'Ação operacional inválida.' });
1374
+ } catch (error) {
1375
+ sendJson(req, res, 500, { error: error?.message || 'Falha ao executar ação operacional.' });
1376
+ }
1377
+ };
1378
+
1379
+ const handleAdminSearchRequest = async (req, res, url) => {
1380
+ const adminSession = requireAdminPanelSession(req, res);
1381
+ if (!adminSession) return;
1382
+ if (!['GET', 'HEAD'].includes(req.method || '')) {
1383
+ sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
1384
+ return;
1385
+ }
1386
+
1387
+ const q = sanitizeText(url?.searchParams?.get('q') || '', 120, { allowEmpty: true }) || '';
1388
+ const limit = Math.max(1, Math.min(30, Number(url?.searchParams?.get('limit') || 12)));
1389
+ if (!q) {
1390
+ sendJson(req, res, 200, {
1391
+ data: {
1392
+ q: '',
1393
+ totals: { users: 0, sessions: 0, groups: 0, packs: 0 },
1394
+ results: { users: [], sessions: [], groups: [], packs: [] },
1395
+ },
1396
+ });
1397
+ return;
1398
+ }
1399
+
1400
+ const like = `%${q}%`;
1401
+
1402
+ const [usersRows, sessionsRows, groupsRows, packs] = await Promise.all([
1403
+ executeQuery(
1404
+ `SELECT google_sub, email, name, owner_jid, owner_phone, last_seen_at, last_login_at
1405
+ FROM ${TABLES.STICKER_WEB_GOOGLE_USER}
1406
+ WHERE google_sub LIKE ? OR email LIKE ? OR name LIKE ? OR owner_jid LIKE ? OR owner_phone LIKE ?
1407
+ ORDER BY COALESCE(last_seen_at, last_login_at, created_at) DESC
1408
+ LIMIT ${limit}`,
1409
+ [like, like, like, like, like],
1410
+ ).catch(() => []),
1411
+ executeQuery(
1412
+ `SELECT session_token, google_sub, email, name, owner_jid, owner_phone, last_seen_at, expires_at
1413
+ FROM ${TABLES.STICKER_WEB_GOOGLE_SESSION}
1414
+ WHERE revoked_at IS NULL
1415
+ AND expires_at > UTC_TIMESTAMP()
1416
+ AND (session_token LIKE ? OR google_sub LIKE ? OR email LIKE ? OR name LIKE ? OR owner_jid LIKE ? OR owner_phone LIKE ?)
1417
+ ORDER BY COALESCE(last_seen_at, created_at) DESC
1418
+ LIMIT ${limit}`,
1419
+ [like, like, like, like, like, like],
1420
+ ).catch(() => []),
1421
+ executeQuery(
1422
+ `SELECT
1423
+ gm.id,
1424
+ COALESCE(NULLIF(gm.subject, ''), ch.name, gm.id) AS subject,
1425
+ gm.owner_jid,
1426
+ gm.updated_at
1427
+ FROM ${TABLES.GROUPS_METADATA} gm
1428
+ LEFT JOIN ${TABLES.CHATS} ch ON ch.id = gm.id
1429
+ WHERE gm.id LIKE ? OR gm.subject LIKE ? OR ch.name LIKE ? OR gm.owner_jid LIKE ?
1430
+ ORDER BY gm.updated_at DESC
1431
+ LIMIT ${limit}`,
1432
+ [like, like, like, like],
1433
+ ).catch(() => []),
1434
+ listAdminPacks({
1435
+ searchParams: new URLSearchParams([
1436
+ ['q', q],
1437
+ ['limit', String(limit)],
1438
+ ]),
1439
+ }).catch(() => []),
1440
+ ]);
1441
+
1442
+ const users = (Array.isArray(usersRows) ? usersRows : []).map((row) => ({
1443
+ google_sub: normalizeGoogleSubject(row?.google_sub),
1444
+ email: normalizeEmail(row?.email) || null,
1445
+ name: sanitizeText(row?.name || '', 120, { allowEmpty: true }) || null,
1446
+ owner_jid: normalizeJid(row?.owner_jid) || null,
1447
+ owner_phone: toWhatsAppPhoneDigits(row?.owner_phone || row?.owner_jid) || null,
1448
+ last_seen_at: toIsoOrNull(row?.last_seen_at),
1449
+ last_login_at: toIsoOrNull(row?.last_login_at),
1450
+ }));
1451
+
1452
+ const sessions = (Array.isArray(sessionsRows) ? sessionsRows : []).map((row) => ({
1453
+ session_token: String(row?.session_token || '').trim() || null,
1454
+ google_sub: normalizeGoogleSubject(row?.google_sub),
1455
+ email: normalizeEmail(row?.email) || null,
1456
+ name: sanitizeText(row?.name || '', 120, { allowEmpty: true }) || null,
1457
+ owner_jid: normalizeJid(row?.owner_jid) || null,
1458
+ owner_phone: toWhatsAppPhoneDigits(row?.owner_phone || row?.owner_jid) || null,
1459
+ last_seen_at: toIsoOrNull(row?.last_seen_at),
1460
+ expires_at: toIsoOrNull(row?.expires_at),
1461
+ }));
1462
+
1463
+ const groups = (Array.isArray(groupsRows) ? groupsRows : []).map((row) => ({
1464
+ id: String(row?.id || '').trim(),
1465
+ subject: sanitizeText(row?.subject || row?.id || '', 255, { allowEmpty: true }) || String(row?.id || '').trim(),
1466
+ owner_jid: normalizeJid(row?.owner_jid) || null,
1467
+ updated_at: toIsoOrNull(row?.updated_at),
1468
+ }));
1469
+
1470
+ sendJson(req, res, 200, {
1471
+ data: {
1472
+ q,
1473
+ totals: {
1474
+ users: users.length,
1475
+ sessions: sessions.length,
1476
+ groups: groups.length,
1477
+ packs: Array.isArray(packs) ? packs.length : 0,
1478
+ },
1479
+ results: {
1480
+ users,
1481
+ sessions,
1482
+ groups,
1483
+ packs: Array.isArray(packs) ? packs : [],
1484
+ },
1485
+ },
1486
+ });
1487
+ };
1488
+
1489
+ const toCsvValue = (value) => {
1490
+ const normalized = value === null || value === undefined ? '' : String(value);
1491
+ if (/[",\n;]/.test(normalized)) {
1492
+ return `"${normalized.replaceAll('"', '""')}"`;
1493
+ }
1494
+ return normalized;
1495
+ };
1496
+
1497
+ const buildCsv = (rows = [], headers = []) => {
1498
+ const safeRows = Array.isArray(rows) ? rows : [];
1499
+ const safeHeaders = Array.isArray(headers) ? headers : [];
1500
+ const lines = [];
1501
+ lines.push(safeHeaders.map((header) => toCsvValue(header)).join(','));
1502
+ for (const row of safeRows) {
1503
+ lines.push(
1504
+ safeHeaders
1505
+ .map((header) => {
1506
+ const value = row && typeof row === 'object' ? row[header] : '';
1507
+ return toCsvValue(value);
1508
+ })
1509
+ .join(','),
1510
+ );
1511
+ }
1512
+ return `${lines.join('\n')}\n`;
1513
+ };
1514
+
1515
+ const handleAdminExportRequest = async (req, res, url) => {
1516
+ const adminSession = requireAdminPanelSession(req, res);
1517
+ if (!adminSession) return;
1518
+ if (!['GET', 'HEAD'].includes(req.method || '')) {
1519
+ sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
1520
+ return;
1521
+ }
1522
+
1523
+ const format = String(url?.searchParams?.get('format') || 'json')
1524
+ .trim()
1525
+ .toLowerCase();
1526
+ const type = String(url?.searchParams?.get('type') || 'metrics')
1527
+ .trim()
1528
+ .toLowerCase();
1529
+
1530
+ const overview = await buildAdminOverviewPayload({ adminSession });
1531
+ const exportData =
1532
+ type === 'events'
1533
+ ? {
1534
+ moderation_queue: overview?.moderation_queue || [],
1535
+ audit_log: overview?.audit_log || [],
1536
+ blocked_accounts: overview?.users_sessions?.blocked_accounts || [],
1537
+ }
1538
+ : {
1539
+ dashboard_quick: overview?.dashboard_quick || null,
1540
+ counters: overview?.counters || null,
1541
+ system_health: overview?.system_health || null,
1542
+ alerts: overview?.alerts || [],
1543
+ feature_flags: overview?.feature_flags || [],
1544
+ };
1545
+
1546
+ await createAdminActionAuditEvent({
1547
+ adminSession,
1548
+ action: 'export_data',
1549
+ targetType: 'admin_export',
1550
+ targetId: `${type}.${format}`,
1551
+ details: { type, format },
1552
+ });
1553
+
1554
+ if (format !== 'csv') {
1555
+ sendJson(req, res, 200, {
1556
+ data: {
1557
+ type,
1558
+ format: 'json',
1559
+ exported_at: new Date().toISOString(),
1560
+ payload: exportData,
1561
+ },
1562
+ });
1563
+ return;
1564
+ }
1565
+
1566
+ let headers = [];
1567
+ let rows = [];
1568
+
1569
+ if (type === 'events') {
1570
+ headers = ['section', 'id', 'event_type', 'severity', 'title', 'subtitle', 'status', 'created_at'];
1571
+ rows = [
1572
+ ...(Array.isArray(exportData?.moderation_queue) ? exportData.moderation_queue : []).map((item) => ({
1573
+ section: 'moderation_queue',
1574
+ id: item?.id || '',
1575
+ event_type: item?.event_type || '',
1576
+ severity: item?.severity || '',
1577
+ title: item?.title || '',
1578
+ subtitle: item?.subtitle || '',
1579
+ status: item?.revoked_at ? 'revoked' : item?.status || '',
1580
+ created_at: item?.created_at || item?.revoked_at || '',
1581
+ })),
1582
+ ...(Array.isArray(exportData?.audit_log) ? exportData.audit_log : []).map((item) => ({
1583
+ section: 'audit_log',
1584
+ id: item?.id || '',
1585
+ event_type: item?.action || '',
1586
+ severity: item?.status || '',
1587
+ title: item?.target_type || '',
1588
+ subtitle: item?.target_id || '',
1589
+ status: item?.status || '',
1590
+ created_at: item?.created_at || '',
1591
+ })),
1592
+ ...(Array.isArray(exportData?.blocked_accounts) ? exportData.blocked_accounts : []).map((item) => ({
1593
+ section: 'blocked_accounts',
1594
+ id: item?.id || '',
1595
+ event_type: 'ban',
1596
+ severity: item?.revoked_at ? 'low' : 'critical',
1597
+ title: item?.email || item?.owner_jid || item?.google_sub || '',
1598
+ subtitle: item?.reason || '',
1599
+ status: item?.revoked_at ? 'revoked' : 'active',
1600
+ created_at: item?.created_at || '',
1601
+ })),
1602
+ ];
1603
+ } else {
1604
+ headers = ['section', 'key', 'value'];
1605
+ const dashboard = exportData?.dashboard_quick || {};
1606
+ const counters = exportData?.counters || {};
1607
+ const health = exportData?.system_health || {};
1608
+ const alerts = Array.isArray(exportData?.alerts) ? exportData.alerts : [];
1609
+ const flags = Array.isArray(exportData?.feature_flags) ? exportData.feature_flags : [];
1610
+ rows = [...Object.entries(dashboard).map(([key, value]) => ({ section: 'dashboard_quick', key, value })), ...Object.entries(counters).map(([key, value]) => ({ section: 'counters', key, value })), ...Object.entries(health).map(([key, value]) => ({ section: 'system_health', key, value })), ...alerts.map((alert, index) => ({ section: 'alerts', key: `${index + 1}:${alert?.code || 'alert'}`, value: `${alert?.severity || ''} ${alert?.title || ''}`.trim() })), ...flags.map((flag) => ({ section: 'feature_flags', key: flag?.flag_name || '', value: `${flag?.is_enabled ? 'on' : 'off'} (${flag?.rollout_percent || 0}%)` }))];
1611
+ }
1612
+
1613
+ const csv = buildCsv(rows, headers);
1614
+ const stamp = new Date().toISOString().slice(0, 19).replace(/[:T]/g, '-');
1615
+ res.statusCode = 200;
1616
+ res.setHeader('Content-Type', 'text/csv; charset=utf-8');
1617
+ res.setHeader('Content-Disposition', `attachment; filename="admin-${type}-${stamp}.csv"`);
1618
+ res.end(csv);
1619
+ };
1620
+
1621
+ const handleAdminModeratorsRequest = async (req, res) => {
1622
+ const adminSession = requireOwnerAdminPanelSession(req, res);
1623
+ if (!adminSession) return;
1624
+
1625
+ if (req.method === 'GET' || req.method === 'HEAD') {
1626
+ const moderators = await listAdminModerators({ activeOnly: false, limit: 500 });
1627
+ sendJson(req, res, 200, { data: { moderators } });
1628
+ return;
1629
+ }
1630
+
1631
+ if (req.method !== 'POST') {
1632
+ sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
1633
+ return;
1634
+ }
1635
+
1636
+ let payload = {};
1637
+ try {
1638
+ payload = await readJsonBody(req);
1639
+ } catch (error) {
1640
+ sendJson(req, res, Number(error?.statusCode || 400), { error: error?.message || 'Body inválido.' });
1641
+ return;
1642
+ }
1643
+
1644
+ try {
1645
+ const result = await upsertAdminModeratorRecord({
1646
+ googleSub: payload?.google_sub,
1647
+ email: payload?.email,
1648
+ ownerJid: payload?.owner_jid,
1649
+ password: payload?.password,
1650
+ adminSession,
1651
+ });
1652
+ await createAdminActionAuditEvent({
1653
+ adminSession,
1654
+ action: result.created ? 'moderator_create' : 'moderator_update',
1655
+ targetType: 'moderator',
1656
+ targetId: result?.moderator?.google_sub || payload?.google_sub || '',
1657
+ details: { created: Boolean(result.created) },
1658
+ });
1659
+ sendJson(req, res, result.created ? 201 : 200, {
1660
+ data: {
1661
+ created: result.created,
1662
+ moderator: result.moderator,
1663
+ },
1664
+ });
1665
+ } catch (error) {
1666
+ sendJson(req, res, Number(error?.statusCode || 400), { error: error?.message || 'Falha ao salvar moderador.' });
1667
+ }
1668
+ };
1669
+
1670
+ const handleAdminModeratorDeleteRequest = async (req, res, googleSub) => {
1671
+ const adminSession = requireOwnerAdminPanelSession(req, res);
1672
+ if (!adminSession) return;
1673
+ if (req.method !== 'DELETE') {
1674
+ sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
1675
+ return;
1676
+ }
1677
+ try {
1678
+ const moderator = await revokeAdminModeratorRecord(googleSub, adminSession);
1679
+ await createAdminActionAuditEvent({
1680
+ adminSession,
1681
+ action: 'moderator_revoke',
1682
+ targetType: 'moderator',
1683
+ targetId: moderator?.google_sub || googleSub,
1684
+ });
1685
+ sendJson(req, res, 200, { data: { revoked: true, moderator } });
1686
+ } catch (error) {
1687
+ sendJson(req, res, Number(error?.statusCode || 400), { error: error?.message || 'Falha ao remover moderador.' });
1688
+ }
1689
+ };
1690
+
1691
+ const handleAdminPacksRequest = async (req, res, url) => {
1692
+ const adminSession = requireAdminPanelSession(req, res);
1693
+ if (!adminSession) return;
1694
+ if (!['GET', 'HEAD'].includes(req.method || '')) {
1695
+ sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
1696
+ return;
1697
+ }
1698
+ const packs = await listAdminPacks(url);
1699
+ sendJson(req, res, 200, { data: packs });
1700
+ };
1701
+
1702
+ const handleAdminPackDetailsRequest = async (req, res, packKey) => {
1703
+ const adminSession = requireAdminPanelSession(req, res);
1704
+ if (!adminSession) return;
1705
+ if (!['GET', 'HEAD'].includes(req.method || '')) {
1706
+ sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
1707
+ return;
1708
+ }
1709
+ const context = await findAdminPackContextByKey(packKey);
1710
+ if (!context?.fullPack) {
1711
+ sendJson(req, res, 404, { error: 'Pack nao encontrado.' });
1712
+ return;
1713
+ }
1714
+ const data = await buildManagedPackResponseData(context.fullPack);
1715
+ sendJson(req, res, 200, { data });
1716
+ };
1717
+
1718
+ const handleAdminPackDeleteRequest = async (req, res, packKey) => {
1719
+ const adminSession = requireAdminPanelSession(req, res);
1720
+ if (!adminSession) return;
1721
+ if (req.method !== 'DELETE') {
1722
+ sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
1723
+ return;
1724
+ }
1725
+ const context = await findAdminPackContextByKey(packKey);
1726
+ const normalizedPackKey = sanitizeText(packKey, 160, { allowEmpty: false }) || String(packKey || '');
1727
+ if (!context?.fullPack) {
1728
+ sendManagedMutationStatus(req, res, 'already_deleted', {
1729
+ deleted: false,
1730
+ pack_key: normalizedPackKey,
1731
+ admin: true,
1732
+ });
1733
+ return;
1734
+ }
1735
+ const result = await deleteManagedPackWithCleanup({
1736
+ ownerJid: context.ownerJid,
1737
+ identifier: context.packKey,
1738
+ fallbackPack: context.fullPack,
1739
+ });
1740
+ await createAdminActionAuditEvent({
1741
+ adminSession,
1742
+ action: 'pack_delete',
1743
+ targetType: 'pack',
1744
+ targetId: result?.deletedPack?.pack_key || context.packKey || packKey,
1745
+ details: {
1746
+ removed_sticker_count: Number(result?.removedCount || 0),
1747
+ missing: Boolean(result?.missing),
1748
+ },
1749
+ });
1750
+ sendManagedMutationStatus(req, res, 'deleted', {
1751
+ admin: true,
1752
+ deleted: !result?.missing,
1753
+ pack_key: result?.deletedPack?.pack_key || context.packKey,
1754
+ id: result?.deletedPack?.id || context.fullPack?.id || null,
1755
+ deleted_at: toIsoOrNull(result?.deletedPack?.deleted_at || new Date()),
1756
+ removed_sticker_count: Number(result?.removedCount || 0),
1757
+ });
1758
+ };
1759
+
1760
+ const handleAdminPackStickerDeleteRequest = async (req, res, packKey, stickerId) => {
1761
+ const adminSession = requireAdminPanelSession(req, res);
1762
+ if (!adminSession) return;
1763
+ if (req.method !== 'DELETE') {
1764
+ sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
1765
+ return;
1766
+ }
1767
+ const context = await findAdminPackContextByKey(packKey);
1768
+ const normalizedStickerId = sanitizeText(stickerId, 36, { allowEmpty: false });
1769
+ if (!context?.fullPack) {
1770
+ sendManagedMutationStatus(req, res, 'already_deleted', {
1771
+ admin: true,
1772
+ pack_key: sanitizeText(packKey, 160, { allowEmpty: true }) || String(packKey || ''),
1773
+ sticker_id: normalizedStickerId || null,
1774
+ });
1775
+ return;
1776
+ }
1777
+ try {
1778
+ const result = await stickerPackService.removeStickerFromPack({
1779
+ ownerJid: context.ownerJid,
1780
+ identifier: context.packKey,
1781
+ selector: normalizedStickerId,
1782
+ });
1783
+ invalidateStickerCatalogDerivedCaches();
1784
+ if (normalizedStickerId) {
1785
+ await cleanupOrphanStickerAssets([normalizedStickerId], { reason: 'admin_remove_sticker' });
1786
+ }
1787
+ await createAdminActionAuditEvent({
1788
+ adminSession,
1789
+ action: 'pack_sticker_delete',
1790
+ targetType: 'sticker',
1791
+ targetId: normalizedStickerId || stickerId,
1792
+ details: {
1793
+ pack_key: context.packKey,
1794
+ },
1795
+ });
1796
+ await sendManagedPackMutationStatus(req, res, 'updated', result?.pack || context.fullPack, {
1797
+ admin: true,
1798
+ pack_key: context.packKey,
1799
+ removed_sticker_id: normalizedStickerId || null,
1800
+ });
1801
+ } catch (error) {
1802
+ const mapped = mapStickerPackWebManageError(error);
1803
+ sendJson(req, res, mapped.statusCode, { error: mapped.message, code: mapped.code });
1804
+ }
1805
+ };
1806
+
1807
+ const handleAdminGlobalStickerDeleteRequest = async (req, res, stickerId) => {
1808
+ const adminSession = requireAdminPanelSession(req, res);
1809
+ if (!adminSession) return;
1810
+ if (req.method !== 'DELETE') {
1811
+ sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
1812
+ return;
1813
+ }
1814
+ const normalizedStickerId = sanitizeText(stickerId, 36, { allowEmpty: false });
1815
+ if (!normalizedStickerId) {
1816
+ sendJson(req, res, 400, { error: 'sticker_id invalido.' });
1817
+ return;
1818
+ }
1819
+
1820
+ const refs = await executeQuery(
1821
+ `SELECT p.pack_key, p.owner_jid
1822
+ FROM ${TABLES.STICKER_PACK_ITEM} i
1823
+ INNER JOIN ${TABLES.STICKER_PACK} p ON p.id = i.pack_id
1824
+ WHERE i.sticker_id = ?
1825
+ AND p.deleted_at IS NULL`,
1826
+ [normalizedStickerId],
1827
+ );
1828
+
1829
+ let removedFromPacks = 0;
1830
+ let removeErrors = 0;
1831
+ for (const ref of Array.isArray(refs) ? refs : []) {
1832
+ try {
1833
+ const ownerJid = normalizeJid(ref.owner_jid) || '';
1834
+ const packKey = sanitizeText(ref.pack_key, 160, { allowEmpty: false });
1835
+ if (!ownerJid || !packKey) continue;
1836
+ await stickerPackService.removeStickerFromPack({
1837
+ ownerJid,
1838
+ identifier: packKey,
1839
+ selector: normalizedStickerId,
1840
+ });
1841
+ removedFromPacks += 1;
1842
+ } catch {
1843
+ removeErrors += 1;
1844
+ }
1845
+ }
1846
+
1847
+ const cleanup = await cleanupOrphanStickerAssets([normalizedStickerId], { reason: 'admin_delete_sticker_global' });
1848
+ invalidateStickerCatalogDerivedCaches();
1849
+ await createAdminActionAuditEvent({
1850
+ adminSession,
1851
+ action: 'global_sticker_delete',
1852
+ targetType: 'sticker',
1853
+ targetId: normalizedStickerId,
1854
+ details: {
1855
+ removed_from_packs: removedFromPacks,
1856
+ remove_errors: removeErrors,
1857
+ cleanup_deleted: Number(cleanup?.deleted || 0),
1858
+ },
1859
+ });
1860
+ sendJson(req, res, 200, {
1861
+ data: {
1862
+ success: true,
1863
+ sticker_id: normalizedStickerId,
1864
+ removed_from_packs: removedFromPacks,
1865
+ remove_errors: removeErrors,
1866
+ cleanup,
1867
+ deleted: Number(cleanup?.deleted || 0) > 0,
1868
+ },
1869
+ });
1870
+ };
1871
+
1872
+ const handleAdminBansRequest = async (req, res) => {
1873
+ const adminSession = requireAdminPanelSession(req, res);
1874
+ if (!adminSession) return;
1875
+
1876
+ if (req.method === 'GET' || req.method === 'HEAD') {
1877
+ const bans = await listAdminBans({ activeOnly: false, limit: 200 });
1878
+ sendJson(req, res, 200, { data: bans });
1879
+ return;
1880
+ }
1881
+
1882
+ if (req.method !== 'POST') {
1883
+ sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
1884
+ return;
1885
+ }
1886
+
1887
+ let payload = {};
1888
+ try {
1889
+ payload = await readJsonBody(req);
1890
+ } catch (error) {
1891
+ sendJson(req, res, Number(error?.statusCode || 400), { error: error?.message || 'Body inválido.' });
1892
+ return;
1893
+ }
1894
+
1895
+ try {
1896
+ const result = await createAdminBanRecord({
1897
+ googleSub: payload?.google_sub,
1898
+ email: payload?.email,
1899
+ ownerJid: payload?.owner_jid,
1900
+ reason: payload?.reason,
1901
+ adminSession,
1902
+ });
1903
+ await createAdminActionAuditEvent({
1904
+ adminSession,
1905
+ action: result.created ? 'ban_create' : 'ban_existing',
1906
+ targetType: 'ban',
1907
+ targetId: result?.ban?.id || '',
1908
+ details: {
1909
+ google_sub: result?.ban?.google_sub || payload?.google_sub || null,
1910
+ email: result?.ban?.email || payload?.email || null,
1911
+ owner_jid: result?.ban?.owner_jid || payload?.owner_jid || null,
1912
+ },
1913
+ });
1914
+ sendJson(req, res, result.created ? 201 : 200, {
1915
+ data: {
1916
+ created: result.created,
1917
+ ban: result.ban,
1918
+ },
1919
+ });
1920
+ } catch (error) {
1921
+ sendJson(req, res, Number(error?.statusCode || 400), { error: error?.message || 'Falha ao banir usuário.' });
1922
+ }
1923
+ };
1924
+
1925
+ const handleAdminBanRevokeRequest = async (req, res, banId) => {
1926
+ const adminSession = requireAdminPanelSession(req, res);
1927
+ if (!adminSession) return;
1928
+ if (req.method !== 'DELETE') {
1929
+ sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
1930
+ return;
1931
+ }
1932
+ try {
1933
+ const ban = await revokeAdminBanRecord(banId);
1934
+ await createAdminActionAuditEvent({
1935
+ adminSession,
1936
+ action: 'ban_revoke',
1937
+ targetType: 'ban',
1938
+ targetId: ban?.id || banId,
1939
+ });
1940
+ sendJson(req, res, 200, { data: { revoked: true, ban } });
1941
+ } catch (error) {
1942
+ sendJson(req, res, Number(error?.statusCode || 400), { error: error?.message || 'Falha ao revogar ban.' });
1943
+ }
1944
+ };
1945
+
1946
+ return {
1947
+ handleAdminPanelSessionRequest,
1948
+ handleAdminOverviewRequest,
1949
+ handleAdminUsersRequest,
1950
+ handleAdminForceLogoutRequest,
1951
+ handleAdminFeatureFlagsRequest,
1952
+ handleAdminOpsActionRequest,
1953
+ handleAdminSearchRequest,
1954
+ handleAdminExportRequest,
1955
+ handleAdminModeratorsRequest,
1956
+ handleAdminModeratorDeleteRequest,
1957
+ handleAdminPacksRequest,
1958
+ handleAdminPackDetailsRequest,
1959
+ handleAdminPackDeleteRequest,
1960
+ handleAdminPackStickerDeleteRequest,
1961
+ handleAdminGlobalStickerDeleteRequest,
1962
+ handleAdminBansRequest,
1963
+ handleAdminBanRevokeRequest,
1964
+ };
1965
+ };