@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
@@ -1,36 +1,38 @@
1
1
  import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
- import { createHash, randomBytes, randomUUID, scryptSync, timingSafeEqual } from 'node:crypto';
4
- import { URL, URLSearchParams } from 'node:url';
5
-
6
- import { executeQuery, pool, TABLES } from '../../database/index.js';
7
- import { getJidUser, normalizeJid, resolveBotJid } from '../../app/config/baileysConfig.js';
8
- import { getAdminPhone, getAdminRawValue, resolveAdminJid } from '../../app/config/adminIdentity.js';
9
- import { getActiveSocket } from '../../app/services/socketState.js';
10
- import { extractUserIdInfo, resolveUserId } from '../../app/services/lidMapService.js';
11
- import { resolveWhatsAppOwnerJidFromLoginPayload, toWhatsAppOwnerJid, toWhatsAppPhoneDigits } from '../../app/services/whatsappLoginLinkService.js';
12
- import logger from '../../app/utils/logger/loggerModule.js';
13
- import { getSystemMetrics } from '../../app/utils/systemMetrics/systemMetricsModule.js';
14
- import { listStickerPacksForCatalog, findStickerPackByPackKey, listStickerPacksByOwner, bumpStickerPackVersion, findStickerPackByOwnerAndIdentifier, softDeleteStickerPack, updateStickerPackFields } from '../../app/modules/stickerPackModule/stickerPackRepository.js';
15
- import { listStickerPackItems, countStickerPackItemRefsByStickerId, createStickerPackItem, getStickerPackItemByStickerId, removeStickerPackItemByStickerId, removeStickerPackItemsByPackId } from '../../app/modules/stickerPackModule/stickerPackItemRepository.js';
16
- import { listClassifiedStickerAssetsWithoutPack, listStickerAssetsWithoutPack, deleteStickerAssetById, findStickerAssetsByIds } from '../../app/modules/stickerPackModule/stickerAssetRepository.js';
17
- import { deleteStickerAssetClassificationByAssetId, findStickerClassificationByAssetId, listStickerClassificationsByAssetIds } from '../../app/modules/stickerPackModule/stickerAssetClassificationRepository.js';
18
- import { decoratePackClassificationSummary, decorateStickerClassification, getPackClassificationSummaryByAssetIds } from '../../app/modules/stickerPackModule/stickerClassificationService.js';
19
- import { getEmptyStickerPackEngagement, getStickerPackEngagementByPackId, incrementStickerPackDislike, incrementStickerPackLike, incrementStickerPackOpen, listStickerPackEngagementByPackIds } from '../../app/modules/stickerPackModule/stickerPackEngagementRepository.js';
20
- import { createStickerPackInteractionEvent, listStickerPackInteractionStatsByPackIds, listViewerRecentPackIds } from '../../app/modules/stickerPackModule/stickerPackInteractionEventRepository.js';
21
- import { buildCreatorRanking, buildIntentCollections, buildPersonalizedRecommendations, buildViewerTagAffinity, computePackSignals } from '../../app/modules/stickerPackModule/stickerPackMarketplaceService.js';
22
- import { listStickerPackScoreSnapshotsByPackIds } from '../../app/modules/stickerPackModule/stickerPackScoreSnapshotRepository.js';
23
- import { createCatalogApiRouter } from '../routes/stickerCatalog/catalogRouter.js';
24
- import { createStickerCatalogNonCatalogHandlers } from './stickerCatalog/nonCatalogHandlers.js';
25
- import { createGoogleWebAuthService } from '../auth/googleWebAuth/googleWebAuthService.js';
26
- import { buildAdminMenu, buildAiMenu, buildAnimeMenu, buildMediaMenu, buildMenuCaption, buildQuoteMenu, buildStatsMenu, buildStickerMenu } from '../../app/modules/menuModule/common.js';
27
- import { getMarketplaceDriftSnapshot } from '../../app/modules/stickerPackModule/stickerMarketplaceDriftService.js';
28
- import { getStickerAssetExternalUrl, getStickerStorageConfig, readStickerAssetBuffer, saveStickerAssetFromBuffer } from '../../app/modules/stickerPackModule/stickerStorageService.js';
29
- import { convertToWebp } from '../../app/modules/stickerModule/convertToWebp.js';
30
- import { sanitizeText } from '../../app/modules/stickerPackModule/stickerPackUtils.js';
31
- import stickerPackService from '../../app/modules/stickerPackModule/stickerPackServiceRuntime.js';
32
- import { STICKER_PACK_ERROR_CODES, StickerPackError } from '../../app/modules/stickerPackModule/stickerPackErrors.js';
33
- import { getFeatureFlagsSnapshot, isFeatureEnabled, refreshFeatureFlags } from '../../app/services/featureFlagService.js';
3
+ import { createHash, randomUUID } from 'node:crypto';
4
+ import { URL } from 'node:url';
5
+
6
+ import { executeQuery, pool, TABLES } from '../../../database/index.js';
7
+ import { getJidUser, normalizeJid, resolveBotJid } from '../../../app/config/baileysConfig.js';
8
+ import { getAdminPhone, getAdminRawValue, resolveAdminJid } from '../../../app/config/adminIdentity.js';
9
+ import { getActiveSocket } from '../../../app/services/socketState.js';
10
+ import { extractUserIdInfo, resolveUserId } from '../../../app/services/lidMapService.js';
11
+ import { resolveWhatsAppOwnerJidFromLoginPayload, toWhatsAppOwnerJid, toWhatsAppPhoneDigits } from '../../../app/services/whatsappLoginLinkService.js';
12
+ import logger from '../../../app/utils/logger/loggerModule.js';
13
+ import { getSystemMetrics } from '../../../app/utils/systemMetrics/systemMetricsModule.js';
14
+ import { listStickerPacksForCatalog, findStickerPackByPackKey, listStickerPacksByOwner, bumpStickerPackVersion, findStickerPackByOwnerAndIdentifier, softDeleteStickerPack, updateStickerPackFields } from '../../../app/modules/stickerPackModule/stickerPackRepository.js';
15
+ import { listStickerPackItems, countStickerPackItemRefsByStickerId, createStickerPackItem, getStickerPackItemByStickerId, removeStickerPackItemByStickerId, removeStickerPackItemsByPackId } from '../../../app/modules/stickerPackModule/stickerPackItemRepository.js';
16
+ import { listClassifiedStickerAssetsWithoutPack, listStickerAssetsWithoutPack, deleteStickerAssetById, findStickerAssetsByIds } from '../../../app/modules/stickerPackModule/stickerAssetRepository.js';
17
+ import { deleteStickerAssetClassificationByAssetId, findStickerClassificationByAssetId, listStickerClassificationsByAssetIds } from '../../../app/modules/stickerPackModule/stickerAssetClassificationRepository.js';
18
+ import { decoratePackClassificationSummary, decorateStickerClassification, getPackClassificationSummaryByAssetIds } from '../../../app/modules/stickerPackModule/stickerClassificationService.js';
19
+ import { getEmptyStickerPackEngagement, getStickerPackEngagementByPackId, incrementStickerPackDislike, incrementStickerPackLike, incrementStickerPackOpen, listStickerPackEngagementByPackIds } from '../../../app/modules/stickerPackModule/stickerPackEngagementRepository.js';
20
+ import { createStickerPackInteractionEvent, listStickerPackInteractionStatsByPackIds, listViewerRecentPackIds } from '../../../app/modules/stickerPackModule/stickerPackInteractionEventRepository.js';
21
+ import { buildCreatorRanking, buildIntentCollections, buildPersonalizedRecommendations, buildViewerTagAffinity, computePackSignals } from '../../../app/modules/stickerPackModule/stickerPackMarketplaceService.js';
22
+ import { listStickerPackScoreSnapshotsByPackIds } from '../../../app/modules/stickerPackModule/stickerPackScoreSnapshotRepository.js';
23
+ import { createCatalogApiRouter } from '../../routes/sticker/catalogRouter.js';
24
+ import { createStickerCatalogNonCatalogHandlers } from './nonCatalogHandlers.js';
25
+ import { createGoogleWebAuthService } from '../../auth/googleWebAuth/googleWebAuthService.js';
26
+ import { createStickerCatalogAdminBanService } from '../admin/adminBanService.js';
27
+ import { createStickerCatalogAdminHandlers } from '../admin/adminPanelHandlers.js';
28
+ import { buildAdminMenu, buildAiMenu, buildAnimeMenu, buildMediaMenu, buildMenuCaption, buildQuoteMenu, buildStatsMenu, buildStickerMenu } from '../../../app/modules/menuModule/common.js';
29
+ import { getMarketplaceDriftSnapshot } from '../../../app/modules/stickerPackModule/stickerMarketplaceDriftService.js';
30
+ import { getStickerAssetExternalUrl, getStickerStorageConfig, readStickerAssetBuffer, saveStickerAssetFromBuffer } from '../../../app/modules/stickerPackModule/stickerStorageService.js';
31
+ import { convertToWebp } from '../../../app/modules/stickerModule/convertToWebp.js';
32
+ import { sanitizeText } from '../../../app/modules/stickerPackModule/stickerPackUtils.js';
33
+ import stickerPackService from '../../../app/modules/stickerPackModule/stickerPackServiceRuntime.js';
34
+ import { STICKER_PACK_ERROR_CODES, StickerPackError } from '../../../app/modules/stickerPackModule/stickerPackErrors.js';
35
+ import { getFeatureFlagsSnapshot, isFeatureEnabled, refreshFeatureFlags } from '../../../app/services/featureFlagService.js';
34
36
 
35
37
  const parseEnvBool = (value, fallback) => {
36
38
  if (value === undefined || value === null || value === '') return fallback;
@@ -127,13 +129,6 @@ const PACK_CREATE_MAX_ITEMS = Math.max(1, Number(process.env.STICKER_PACK_MAX_IT
127
129
  const PACK_CREATE_MAX_PACKS_PER_OWNER = parseMaxPacksPerOwnerLimit(process.env.STICKER_PACK_MAX_PACKS_PER_OWNER, 50);
128
130
  const PACK_WEB_EDIT_TOKEN_TTL_MS = Math.max(60_000, Number(process.env.STICKER_WEB_EDIT_TOKEN_TTL_MS) || 6 * 60 * 60 * 1000);
129
131
  const STICKER_WEB_GOOGLE_CLIENT_ID = String(process.env.STICKER_WEB_GOOGLE_CLIENT_ID || '').trim();
130
- const ADMIN_PANEL_EMAIL = String(process.env.ADM_EMAIL || '')
131
- .trim()
132
- .toLowerCase();
133
- const ADMIN_PANEL_PASSWORD = String(process.env.ADM_PANEL_PASSWORD || process.env.ADM_PANEL || '').trim();
134
- const ADMIN_PANEL_ENABLED = Boolean(ADMIN_PANEL_EMAIL && ADMIN_PANEL_PASSWORD);
135
- const ADMIN_PANEL_SESSION_TTL_MS = Math.max(10 * 60 * 1000, Number(process.env.ADM_PANEL_SESSION_TTL_MS) || 12 * 60 * 60 * 1000);
136
- const ADMIN_MODERATOR_PASSWORD_MIN_LENGTH = Math.max(6, Number(process.env.ADM_MODERATOR_PASSWORD_MIN_LENGTH) || 8);
137
132
  const STICKER_WEB_GOOGLE_AUTH_REQUIRED = parseEnvBool(process.env.STICKER_WEB_GOOGLE_AUTH_REQUIRED, Boolean(STICKER_WEB_GOOGLE_CLIENT_ID));
138
133
  const STICKER_WEB_GOOGLE_SESSION_TTL_MS = Math.max(5 * 60 * 1000, Number(process.env.STICKER_WEB_GOOGLE_SESSION_TTL_MS) || 7 * 24 * 60 * 60 * 1000);
139
134
  const STICKER_CATALOG_ONLY_CLASSIFIED = parseEnvBool(process.env.STICKER_CATALOG_ONLY_CLASSIFIED, true);
@@ -186,8 +181,6 @@ const { maxStickerBytes: MAX_STICKER_UPLOAD_BYTES } = getStickerStorageConfig();
186
181
  const MAX_STICKER_SOURCE_UPLOAD_BYTES = Math.max(MAX_STICKER_UPLOAD_BYTES, Number(process.env.STICKER_WEB_UPLOAD_SOURCE_MAX_BYTES) || 20 * 1024 * 1024);
187
182
  const ALLOWED_WEB_UPLOAD_VIDEO_MIMETYPES = new Set(['video/mp4', 'video/webm', 'video/quicktime', 'video/x-m4v']);
188
183
  const webPackEditTokenMap = new Map();
189
- const adminPanelSessionMap = new Map();
190
- const ADMIN_PANEL_SESSION_COOKIE_NAME = 'omnizap_admin_panel_session';
191
184
  const WEB_VISITOR_COOKIE_NAME = 'omnizap_vid';
192
185
  const WEB_SESSION_COOKIE_NAME = 'omnizap_sid';
193
186
  const PACK_WEB_STATUS_VALUES = new Set(['draft', 'uploading', 'processing', 'published', 'failed']);
@@ -203,7 +196,6 @@ const staleDraftCleanupState = {
203
196
  running: false,
204
197
  lastRunAt: 0,
205
198
  };
206
- let adminPanelSessionPruneAt = 0;
207
199
 
208
200
  const hasPathPrefix = (pathname, prefix) => pathname === prefix || pathname.startsWith(`${prefix}/`);
209
201
  const isPackPubliclyVisible = (pack) => {
@@ -488,52 +480,6 @@ const normalizeEmail = (value) =>
488
480
  .trim()
489
481
  .toLowerCase()
490
482
  .slice(0, 255);
491
- const constantTimeStringEqual = (a, b) => {
492
- const left = Buffer.from(String(a || ''), 'utf8');
493
- const right = Buffer.from(String(b || ''), 'utf8');
494
- if (left.length !== right.length) return false;
495
- try {
496
- return timingSafeEqual(left, right);
497
- } catch {
498
- return false;
499
- }
500
- };
501
- const normalizeAdminPanelRole = (value, fallback = 'owner') => {
502
- const normalized = String(value || '')
503
- .trim()
504
- .toLowerCase();
505
- if (normalized === 'moderator') return 'moderator';
506
- if (normalized === 'owner') return 'owner';
507
- return fallback;
508
- };
509
- const hashAdminModeratorPassword = (password) => {
510
- const normalized = String(password || '');
511
- const salt = randomBytes(16).toString('hex');
512
- const hash = scryptSync(normalized, salt, 64).toString('hex');
513
- return `scrypt$${salt}$${hash}`;
514
- };
515
- const verifyAdminModeratorPassword = (password, encodedHash) => {
516
- const raw = String(encodedHash || '').trim();
517
- if (!raw) return false;
518
- const parts = raw.split('$');
519
- if (parts.length !== 3 || parts[0] !== 'scrypt') return false;
520
- const salt = String(parts[1] || '').trim();
521
- const expectedHex = String(parts[2] || '').trim();
522
- if (!salt || !expectedHex) return false;
523
- let expectedBuffer;
524
- try {
525
- expectedBuffer = Buffer.from(expectedHex, 'hex');
526
- } catch {
527
- return false;
528
- }
529
- if (!expectedBuffer.length) return false;
530
- const derived = scryptSync(String(password || ''), salt, expectedBuffer.length);
531
- try {
532
- return timingSafeEqual(expectedBuffer, derived);
533
- } catch {
534
- return false;
535
- }
536
- };
537
483
 
538
484
  const clampUploadErrorMessage = (message) =>
539
485
  String(message || '')
@@ -878,136 +824,26 @@ const buildGoogleOwnerJid = (googleSub) => {
878
824
  return normalizeJid(`g${normalizedSub}@google.oauth`) || '';
879
825
  };
880
826
 
881
- const mapAdminBanRow = (row) => {
882
- if (!row || typeof row !== 'object') return null;
883
- return {
884
- id: String(row.id || '').trim(),
885
- google_sub: normalizeGoogleSubject(row.google_sub),
886
- email: normalizeEmail(row.email),
887
- owner_jid: normalizeJid(row.owner_jid) || null,
888
- reason: sanitizeText(row.reason || '', 255, { allowEmpty: true }) || null,
889
- created_by_google_sub: normalizeGoogleSubject(row.created_by_google_sub),
890
- created_by_email: normalizeEmail(row.created_by_email),
891
- created_at: toIsoOrNull(row.created_at),
892
- updated_at: toIsoOrNull(row.updated_at),
893
- revoked_at: toIsoOrNull(row.revoked_at),
894
- };
895
- };
896
-
897
- const findActiveAdminBanForIdentity = async ({ googleSub = '', email = '', ownerJid = '' } = {}) => {
898
- const normalizedSub = normalizeGoogleSubject(googleSub);
899
- const normalizedEmail = normalizeEmail(email);
900
- const normalizedOwnerJid = normalizeJid(ownerJid) || '';
901
- const clauses = [];
902
- const params = [];
903
- if (normalizedSub) {
904
- clauses.push('google_sub = ?');
905
- params.push(normalizedSub);
906
- }
907
- if (normalizedEmail) {
908
- clauses.push('email = ?');
909
- params.push(normalizedEmail);
910
- }
911
- if (normalizedOwnerJid) {
912
- clauses.push('owner_jid = ?');
913
- params.push(normalizedOwnerJid);
914
- }
915
- if (!clauses.length) return null;
916
-
917
- const rows = await executeQuery(
918
- `SELECT *
919
- FROM ${TABLES.STICKER_WEB_ADMIN_BAN}
920
- WHERE revoked_at IS NULL
921
- AND (${clauses.join(' OR ')})
922
- ORDER BY created_at DESC
923
- LIMIT 1`,
924
- params,
925
- );
926
- return mapAdminBanRow(Array.isArray(rows) ? rows[0] : null);
927
- };
928
-
929
- const listAdminBans = async ({ activeOnly = false, limit = 100 } = {}) => {
930
- const safeLimit = Math.max(1, Math.min(500, Number(limit || 100)));
931
- const rows = await executeQuery(
932
- `SELECT *
933
- FROM ${TABLES.STICKER_WEB_ADMIN_BAN}
934
- ${activeOnly ? 'WHERE revoked_at IS NULL' : ''}
935
- ORDER BY created_at DESC
936
- LIMIT ${safeLimit}`,
937
- );
938
- return (Array.isArray(rows) ? rows : []).map(mapAdminBanRow).filter(Boolean);
939
- };
940
-
941
- const createAdminBanRecord = async ({ googleSub = '', email = '', ownerJid = '', reason = '', adminSession = null }) => {
942
- const normalizedSub = normalizeGoogleSubject(googleSub);
943
- const normalizedEmail = normalizeEmail(email);
944
- const normalizedOwnerJid = normalizeJid(ownerJid) || '';
945
- if (!normalizedSub && !normalizedEmail && !normalizedOwnerJid) {
946
- const error = new Error('Informe google_sub, email ou owner_jid para banir.');
947
- error.statusCode = 400;
948
- throw error;
949
- }
950
- const existing = await findActiveAdminBanForIdentity({
951
- googleSub: normalizedSub,
952
- email: normalizedEmail,
953
- ownerJid: normalizedOwnerJid,
954
- });
955
- if (existing) return { created: false, ban: existing };
956
-
957
- const banId = randomUUID();
958
- await executeQuery(
959
- `INSERT INTO ${TABLES.STICKER_WEB_ADMIN_BAN}
960
- (id, google_sub, email, owner_jid, reason, created_by_google_sub, created_by_email)
961
- VALUES (?, ?, ?, ?, ?, ?, ?)`,
962
- [banId, normalizedSub || null, normalizedEmail || null, normalizedOwnerJid || null, sanitizeText(reason || '', 255, { allowEmpty: true }) || null, normalizeGoogleSubject(adminSession?.googleSub) || null, normalizeEmail(adminSession?.email) || null],
963
- );
964
-
965
- if (normalizedSub || normalizedEmail || normalizedOwnerJid) {
966
- await revokeGoogleWebSessionsByIdentity({
967
- googleSub: normalizedSub,
968
- email: normalizedEmail,
969
- ownerJid: normalizedOwnerJid,
970
- }).catch(() => {});
971
- }
972
-
973
- const rows = await executeQuery(`SELECT * FROM ${TABLES.STICKER_WEB_ADMIN_BAN} WHERE id = ? LIMIT 1`, [banId]);
974
- return { created: true, ban: mapAdminBanRow(Array.isArray(rows) ? rows[0] : null) };
975
- };
827
+ let revokeGoogleWebSessionsByIdentityBridge = async () => 0;
976
828
 
977
- const revokeAdminBanRecord = async (banId) => {
978
- const normalizedId = sanitizeText(banId, 36, { allowEmpty: false });
979
- if (!normalizedId) {
980
- const error = new Error('ban_id invalido.');
981
- error.statusCode = 400;
982
- throw error;
983
- }
984
- await executeQuery(
985
- `UPDATE ${TABLES.STICKER_WEB_ADMIN_BAN}
986
- SET revoked_at = COALESCE(revoked_at, UTC_TIMESTAMP())
987
- WHERE id = ?`,
988
- [normalizedId],
989
- );
990
- const rows = await executeQuery(`SELECT * FROM ${TABLES.STICKER_WEB_ADMIN_BAN} WHERE id = ? LIMIT 1`, [normalizedId]);
991
- return mapAdminBanRow(Array.isArray(rows) ? rows[0] : null);
992
- };
829
+ const adminBanService = createStickerCatalogAdminBanService({
830
+ executeQuery,
831
+ tables: TABLES,
832
+ sanitizeText,
833
+ normalizeGoogleSubject,
834
+ normalizeEmail,
835
+ normalizeJid,
836
+ toIsoOrNull,
837
+ revokeGoogleWebSessionsByIdentity: (payload) => revokeGoogleWebSessionsByIdentityBridge(payload),
838
+ });
993
839
 
994
- const assertGoogleIdentityNotBanned = async ({ sub = '', email = '', ownerJid = '' } = {}) => {
995
- const ban = await findActiveAdminBanForIdentity({ googleSub: sub, email, ownerJid });
996
- if (!ban) return null;
997
- const error = new Error('Conta bloqueada pela administracao.');
998
- error.statusCode = 403;
999
- error.code = 'ADMIN_BANNED';
1000
- error.ban = ban;
1001
- throw error;
1002
- };
840
+ const { listAdminBans, createAdminBanRecord, revokeAdminBanRecord, assertGoogleIdentityNotBanned } = adminBanService;
1003
841
 
1004
842
  const googleWebSessionDbTouchIntervalMs = Math.max(30_000, Number(process.env.STICKER_WEB_GOOGLE_SESSION_DB_TOUCH_INTERVAL_MS) || 60_000);
1005
843
  const googleWebSessionDbPruneIntervalMs = Math.max(5 * 60 * 1000, Number(process.env.STICKER_WEB_GOOGLE_SESSION_DB_PRUNE_INTERVAL_MS) || 60 * 60 * 1000);
1006
844
  const configuredGoogleWebSessionCookiePath = normalizeBasePath(process.env.STICKER_WEB_GOOGLE_SESSION_COOKIE_PATH, '/');
1007
845
  const googleWebSessionCookiePath = '/';
1008
- const googleWebLegacyCookiePaths = Array.from(
1009
- new Set([configuredGoogleWebSessionCookiePath, STICKER_API_BASE_PATH, `${STICKER_API_BASE_PATH}/auth`, STICKER_WEB_PATH, STICKER_LOGIN_WEB_PATH]),
1010
- );
846
+ const googleWebLegacyCookiePaths = Array.from(new Set([configuredGoogleWebSessionCookiePath, STICKER_API_BASE_PATH, `${STICKER_API_BASE_PATH}/auth`, STICKER_WEB_PATH, STICKER_LOGIN_WEB_PATH]));
1011
847
 
1012
848
  const googleWebAuth = createGoogleWebAuthService({
1013
849
  executeQuery,
@@ -1039,380 +875,7 @@ const googleWebAuth = createGoogleWebAuthService({
1039
875
  });
1040
876
 
1041
877
  const { upsertGoogleWebUserRecord, resolveGoogleWebSessionFromRequest, mapGoogleSessionResponseData, handleGoogleAuthSessionRequest, revokeGoogleWebSessionsByIdentity } = googleWebAuth;
1042
-
1043
- const mapAdminModeratorRow = (row) => {
1044
- if (!row || typeof row !== 'object') return null;
1045
- return {
1046
- google_sub: normalizeGoogleSubject(row.google_sub),
1047
- email: normalizeEmail(row.email),
1048
- owner_jid: normalizeJid(row.owner_jid) || null,
1049
- name: sanitizeText(row.name || '', 120, { allowEmpty: true }) || null,
1050
- created_by_google_sub: normalizeGoogleSubject(row.created_by_google_sub),
1051
- created_by_email: normalizeEmail(row.created_by_email),
1052
- updated_by_google_sub: normalizeGoogleSubject(row.updated_by_google_sub),
1053
- updated_by_email: normalizeEmail(row.updated_by_email),
1054
- last_login_at: toIsoOrNull(row.last_login_at),
1055
- created_at: toIsoOrNull(row.created_at),
1056
- updated_at: toIsoOrNull(row.updated_at),
1057
- revoked_at: toIsoOrNull(row.revoked_at),
1058
- active: !row.revoked_at,
1059
- };
1060
- };
1061
-
1062
- const listAdminModerators = async ({ activeOnly = false, limit = 200 } = {}) => {
1063
- const safeLimit = Math.max(1, Math.min(500, Number(limit || 200)));
1064
- const rows = await executeQuery(
1065
- `SELECT google_sub, email, owner_jid, name, created_by_google_sub, created_by_email, updated_by_google_sub, updated_by_email,
1066
- last_login_at, created_at, updated_at, revoked_at
1067
- FROM ${TABLES.STICKER_WEB_ADMIN_MODERATOR}
1068
- ${activeOnly ? 'WHERE revoked_at IS NULL' : ''}
1069
- ORDER BY updated_at DESC
1070
- LIMIT ${safeLimit}`,
1071
- );
1072
- return (Array.isArray(rows) ? rows : []).map(mapAdminModeratorRow).filter(Boolean);
1073
- };
1074
-
1075
- const findAdminModeratorByGoogleSub = async (googleSub, { activeOnly = false } = {}) => {
1076
- const normalizedSub = normalizeGoogleSubject(googleSub);
1077
- if (!normalizedSub) return null;
1078
- const rows = await executeQuery(
1079
- `SELECT *
1080
- FROM ${TABLES.STICKER_WEB_ADMIN_MODERATOR}
1081
- WHERE google_sub = ?
1082
- ${activeOnly ? 'AND revoked_at IS NULL' : ''}
1083
- LIMIT 1`,
1084
- [normalizedSub],
1085
- );
1086
- return Array.isArray(rows) && rows[0] ? rows[0] : null;
1087
- };
1088
-
1089
- const resolveKnownGoogleUserForModerator = async ({ googleSub = '', email = '', ownerJid = '' } = {}) => {
1090
- const normalizedSub = normalizeGoogleSubject(googleSub);
1091
- const normalizedEmail = normalizeEmail(email);
1092
- const normalizedOwnerJid = normalizeJid(ownerJid) || '';
1093
- const clauses = [];
1094
- const params = [];
1095
-
1096
- if (normalizedSub) {
1097
- clauses.push('google_sub = ?');
1098
- params.push(normalizedSub);
1099
- }
1100
- if (normalizedEmail) {
1101
- clauses.push('email = ?');
1102
- params.push(normalizedEmail);
1103
- }
1104
- if (normalizedOwnerJid) {
1105
- clauses.push('owner_jid = ?');
1106
- params.push(normalizedOwnerJid);
1107
- }
1108
- if (!clauses.length) return null;
1109
-
1110
- const rows = await executeQuery(
1111
- `SELECT google_sub, email, owner_jid, name
1112
- FROM ${TABLES.STICKER_WEB_GOOGLE_USER}
1113
- WHERE ${clauses.join(' OR ')}
1114
- ORDER BY COALESCE(last_seen_at, last_login_at, updated_at, created_at) DESC
1115
- LIMIT 1`,
1116
- params,
1117
- );
1118
- const row = Array.isArray(rows) ? rows[0] : null;
1119
- if (!row) return null;
1120
- const resolvedGoogleSub = normalizeGoogleSubject(row.google_sub);
1121
- const resolvedEmail = normalizeEmail(row.email);
1122
- const resolvedOwnerJid = normalizeJid(row.owner_jid) || '';
1123
- if (!resolvedGoogleSub || !resolvedEmail || !resolvedOwnerJid) return null;
1124
- return {
1125
- google_sub: resolvedGoogleSub,
1126
- email: resolvedEmail,
1127
- owner_jid: resolvedOwnerJid,
1128
- name: sanitizeText(row.name || '', 120, { allowEmpty: true }) || null,
1129
- };
1130
- };
1131
-
1132
- const findActiveAdminModeratorForIdentity = async ({ googleSub = '', email = '', ownerJid = '' } = {}) => {
1133
- const normalizedSub = normalizeGoogleSubject(googleSub);
1134
- const normalizedEmail = normalizeEmail(email);
1135
- const normalizedOwnerJid = normalizeJid(ownerJid) || '';
1136
- const clauses = [];
1137
- const params = [];
1138
- if (normalizedSub) {
1139
- clauses.push('google_sub = ?');
1140
- params.push(normalizedSub);
1141
- }
1142
- if (normalizedEmail) {
1143
- clauses.push('email = ?');
1144
- params.push(normalizedEmail);
1145
- }
1146
- if (normalizedOwnerJid) {
1147
- clauses.push('owner_jid = ?');
1148
- params.push(normalizedOwnerJid);
1149
- }
1150
- if (!clauses.length) return null;
1151
-
1152
- const rows = await executeQuery(
1153
- `SELECT *
1154
- FROM ${TABLES.STICKER_WEB_ADMIN_MODERATOR}
1155
- WHERE revoked_at IS NULL
1156
- AND (${clauses.join(' OR ')})
1157
- ORDER BY updated_at DESC
1158
- LIMIT 1`,
1159
- params,
1160
- );
1161
- return Array.isArray(rows) && rows[0] ? rows[0] : null;
1162
- };
1163
-
1164
- const pruneModeratorAdminPanelSessions = ({ googleSub = '', email = '', ownerJid = '' } = {}) => {
1165
- const normalizedSub = normalizeGoogleSubject(googleSub);
1166
- const normalizedEmail = normalizeEmail(email);
1167
- const normalizedOwnerJid = normalizeJid(ownerJid) || '';
1168
- if (!normalizedSub && !normalizedEmail && !normalizedOwnerJid) return;
1169
-
1170
- for (const [token, session] of adminPanelSessionMap.entries()) {
1171
- if (!session || normalizeAdminPanelRole(session.role, 'owner') !== 'moderator') continue;
1172
- const sessionSub = normalizeGoogleSubject(session.googleSub);
1173
- const sessionEmail = normalizeEmail(session.email);
1174
- const sessionOwner = normalizeJid(session.ownerJid) || '';
1175
- if ((normalizedSub && sessionSub === normalizedSub) || (normalizedEmail && sessionEmail === normalizedEmail) || (normalizedOwnerJid && sessionOwner === normalizedOwnerJid)) {
1176
- adminPanelSessionMap.delete(token);
1177
- }
1178
- }
1179
- };
1180
-
1181
- const upsertAdminModeratorRecord = async ({ googleSub = '', email = '', ownerJid = '', password = '', adminSession = null }) => {
1182
- const cleanPassword = String(password || '').trim();
1183
- if (cleanPassword.length < ADMIN_MODERATOR_PASSWORD_MIN_LENGTH) {
1184
- const error = new Error(`Senha do moderador deve ter no minimo ${ADMIN_MODERATOR_PASSWORD_MIN_LENGTH} caracteres.`);
1185
- error.statusCode = 400;
1186
- throw error;
1187
- }
1188
-
1189
- const knownUser = await resolveKnownGoogleUserForModerator({ googleSub, email, ownerJid });
1190
- if (!knownUser?.google_sub) {
1191
- const error = new Error('Somente usuarios Google logados no site podem virar moderadores.');
1192
- error.statusCode = 400;
1193
- throw error;
1194
- }
1195
-
1196
- const existing = await findAdminModeratorByGoogleSub(knownUser.google_sub);
1197
- const passwordHash = hashAdminModeratorPassword(cleanPassword);
1198
- await executeQuery(
1199
- `INSERT INTO ${TABLES.STICKER_WEB_ADMIN_MODERATOR}
1200
- (google_sub, email, owner_jid, name, password_hash, created_by_google_sub, created_by_email, updated_by_google_sub, updated_by_email, revoked_at)
1201
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NULL)
1202
- ON DUPLICATE KEY UPDATE
1203
- email = VALUES(email),
1204
- owner_jid = VALUES(owner_jid),
1205
- name = VALUES(name),
1206
- password_hash = VALUES(password_hash),
1207
- updated_by_google_sub = VALUES(updated_by_google_sub),
1208
- updated_by_email = VALUES(updated_by_email),
1209
- revoked_at = NULL`,
1210
- [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],
1211
- );
1212
-
1213
- pruneModeratorAdminPanelSessions({
1214
- googleSub: knownUser.google_sub,
1215
- email: knownUser.email,
1216
- ownerJid: knownUser.owner_jid,
1217
- });
1218
-
1219
- const fresh = await findAdminModeratorByGoogleSub(knownUser.google_sub);
1220
- return {
1221
- created: !existing,
1222
- moderator: mapAdminModeratorRow(fresh),
1223
- };
1224
- };
1225
-
1226
- const revokeAdminModeratorRecord = async (googleSub, adminSession = null) => {
1227
- const normalizedSub = normalizeGoogleSubject(googleSub);
1228
- if (!normalizedSub) {
1229
- const error = new Error('google_sub invalido.');
1230
- error.statusCode = 400;
1231
- throw error;
1232
- }
1233
-
1234
- await executeQuery(
1235
- `UPDATE ${TABLES.STICKER_WEB_ADMIN_MODERATOR}
1236
- SET revoked_at = COALESCE(revoked_at, UTC_TIMESTAMP()),
1237
- updated_by_google_sub = ?,
1238
- updated_by_email = ?
1239
- WHERE google_sub = ?`,
1240
- [normalizeGoogleSubject(adminSession?.googleSub) || null, normalizeEmail(adminSession?.email) || null, normalizedSub],
1241
- );
1242
-
1243
- const fresh = await findAdminModeratorByGoogleSub(normalizedSub);
1244
- if (!fresh) {
1245
- const error = new Error('Moderador nao encontrado.');
1246
- error.statusCode = 404;
1247
- throw error;
1248
- }
1249
-
1250
- pruneModeratorAdminPanelSessions({
1251
- googleSub: normalizedSub,
1252
- email: normalizeEmail(fresh.email),
1253
- ownerJid: normalizeJid(fresh.owner_jid) || '',
1254
- });
1255
-
1256
- return mapAdminModeratorRow(fresh);
1257
- };
1258
-
1259
- const touchAdminModeratorLastLogin = async (moderatorRow, googleSession) => {
1260
- const normalizedSub = normalizeGoogleSubject(moderatorRow?.google_sub || googleSession?.sub);
1261
- if (!normalizedSub) return;
1262
- await executeQuery(
1263
- `UPDATE ${TABLES.STICKER_WEB_ADMIN_MODERATOR}
1264
- SET email = ?,
1265
- owner_jid = ?,
1266
- name = ?,
1267
- last_login_at = UTC_TIMESTAMP(),
1268
- updated_by_google_sub = ?,
1269
- updated_by_email = ?
1270
- WHERE google_sub = ?`,
1271
- [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],
1272
- ).catch(() => {});
1273
- };
1274
-
1275
- const pruneExpiredAdminPanelSessions = () => {
1276
- const now = Date.now();
1277
- if (now - adminPanelSessionPruneAt < 30_000) return;
1278
- adminPanelSessionPruneAt = now;
1279
- for (const [token, session] of adminPanelSessionMap.entries()) {
1280
- if (!session || Number(session.expiresAt || 0) <= now) {
1281
- adminPanelSessionMap.delete(token);
1282
- }
1283
- }
1284
- };
1285
-
1286
- const getAdminPanelSessionTokenFromRequest = (req) => {
1287
- const direct = getCookieValuesFromRequest(req, ADMIN_PANEL_SESSION_COOKIE_NAME);
1288
- if (direct.length > 0) return direct[0];
1289
- const cookies = parseCookies(req);
1290
- return String(cookies[ADMIN_PANEL_SESSION_COOKIE_NAME] || '').trim();
1291
- };
1292
-
1293
- const clearAdminPanelSessionCookie = (req, res) => {
1294
- appendSetCookie(
1295
- res,
1296
- buildCookieString(ADMIN_PANEL_SESSION_COOKIE_NAME, '', req, {
1297
- maxAgeSeconds: 0,
1298
- }),
1299
- );
1300
- // Also clear host-only variant (legacy cookie written without Domain).
1301
- appendSetCookie(
1302
- res,
1303
- buildCookieString(ADMIN_PANEL_SESSION_COOKIE_NAME, '', req, {
1304
- maxAgeSeconds: 0,
1305
- domain: false,
1306
- }),
1307
- );
1308
- };
1309
-
1310
- const createAdminPanelSession = (googleSession, { role = 'owner' } = {}) => {
1311
- pruneExpiredAdminPanelSessions();
1312
- const now = Date.now();
1313
- const normalizedRole = normalizeAdminPanelRole(role, 'owner');
1314
- const token = randomUUID();
1315
- const session = {
1316
- token,
1317
- role: normalizedRole,
1318
- googleSub: normalizeGoogleSubject(googleSession?.sub),
1319
- ownerJid: normalizeJid(googleSession?.ownerJid) || '',
1320
- email: normalizeEmail(googleSession?.email),
1321
- name: sanitizeText(googleSession?.name || '', 120, { allowEmpty: true }) || 'Administrador',
1322
- picture: String(googleSession?.picture || '').trim() || '',
1323
- createdAt: now,
1324
- expiresAt: now + ADMIN_PANEL_SESSION_TTL_MS,
1325
- };
1326
- adminPanelSessionMap.set(token, session);
1327
- return session;
1328
- };
1329
-
1330
- const resolveAdminPanelSessionFromRequest = (req) => {
1331
- if (!ADMIN_PANEL_ENABLED) return null;
1332
- pruneExpiredAdminPanelSessions();
1333
- const token = getAdminPanelSessionTokenFromRequest(req);
1334
- if (!token) return null;
1335
- const session = adminPanelSessionMap.get(token);
1336
- if (!session) return null;
1337
- if (Number(session.expiresAt || 0) <= Date.now()) {
1338
- adminPanelSessionMap.delete(token);
1339
- return null;
1340
- }
1341
- return session;
1342
- };
1343
-
1344
- const mapAdminPanelSessionResponseData = (session) =>
1345
- session
1346
- ? {
1347
- authenticated: true,
1348
- role: normalizeAdminPanelRole(session.role, 'owner'),
1349
- capabilities: {
1350
- can_manage_moderators: normalizeAdminPanelRole(session.role, 'owner') === 'owner',
1351
- },
1352
- user: {
1353
- google_sub: session.googleSub,
1354
- owner_jid: session.ownerJid,
1355
- email: session.email,
1356
- name: session.name,
1357
- picture: session.picture || null,
1358
- },
1359
- expires_at: toIsoOrNull(session.expiresAt),
1360
- }
1361
- : {
1362
- authenticated: false,
1363
- role: null,
1364
- capabilities: {
1365
- can_manage_moderators: false,
1366
- },
1367
- user: null,
1368
- expires_at: null,
1369
- };
1370
-
1371
- const isOwnerGoogleSessionAllowed = (googleSession) => {
1372
- if (!ADMIN_PANEL_ENABLED) return false;
1373
- if (!googleSession?.sub || !googleSession?.ownerJid) return false;
1374
- const email = normalizeEmail(googleSession.email);
1375
- return Boolean(email && email === ADMIN_PANEL_EMAIL);
1376
- };
1377
-
1378
- const resolveAdminPanelLoginEligibility = async (googleSession) => {
1379
- if (!ADMIN_PANEL_ENABLED || !googleSession?.sub || !googleSession?.ownerJid) {
1380
- return { eligible: false, role: '', moderator: null };
1381
- }
1382
- if (isOwnerGoogleSessionAllowed(googleSession)) {
1383
- return { eligible: true, role: 'owner', moderator: null };
1384
- }
1385
- const moderator = await findActiveAdminModeratorForIdentity({
1386
- googleSub: googleSession.sub,
1387
- email: googleSession.email,
1388
- ownerJid: googleSession.ownerJid,
1389
- });
1390
- if (!moderator) return { eligible: false, role: '', moderator: null };
1391
- return { eligible: true, role: 'moderator', moderator };
1392
- };
1393
-
1394
- const requireAdminPanelSession = (req, res) => {
1395
- if (!ADMIN_PANEL_ENABLED) {
1396
- sendJson(req, res, 404, { error: 'Painel admin desabilitado.' });
1397
- return null;
1398
- }
1399
- const session = resolveAdminPanelSessionFromRequest(req);
1400
- if (!session) {
1401
- sendJson(req, res, 401, { error: 'Sessao admin invalida ou expirada.' });
1402
- return null;
1403
- }
1404
- return session;
1405
- };
1406
-
1407
- const requireOwnerAdminPanelSession = (req, res) => {
1408
- const session = requireAdminPanelSession(req, res);
1409
- if (!session) return null;
1410
- if (normalizeAdminPanelRole(session.role, 'owner') !== 'owner') {
1411
- sendJson(req, res, 403, { error: 'Somente o dono pode gerenciar moderadores.' });
1412
- return null;
1413
- }
1414
- return session;
1415
- };
878
+ revokeGoogleWebSessionsByIdentityBridge = revokeGoogleWebSessionsByIdentity;
1416
879
 
1417
880
  const sendAsset = (req, res, buffer, mimetype = 'image/webp') => {
1418
881
  const maxAgeSeconds = Math.max(60 * 60 * 24, ASSET_CACHE_SECONDS);
@@ -3359,34 +2822,23 @@ const handleMarketplaceStatsRequest = async (req, res, url) => {
3359
2822
  };
3360
2823
 
3361
2824
  const buildHomeRealtimeSnapshot = async ({ systemSummary = null } = {}) => {
3362
- let messagesToday = null;
3363
- let spamBlockedToday = null;
3364
- let analyticsError = null;
3365
-
3366
- try {
3367
- const [row] = await executeQuery(
3368
- `SELECT
3369
- COUNT(*) AS messages_today,
3370
- SUM(CASE WHEN processing_result = 'blocked_antilink' THEN 1 ELSE 0 END) AS spam_blocked_today
3371
- FROM ${TABLES.MESSAGE_ANALYSIS_EVENT}
3372
- WHERE created_at >= UTC_DATE()`,
3373
- );
3374
- messagesToday = Number(row?.messages_today || 0);
3375
- spamBlockedToday = Number(row?.spam_blocked_today || 0);
3376
- } catch (error) {
3377
- analyticsError = error?.message || 'message_analysis_event_unavailable';
3378
- }
3379
-
3380
- const botsOnline = systemSummary?.bot?.connected ? 1 : 0;
3381
- const uptime = String(systemSummary?.process?.uptime || '').trim() || null;
2825
+ const totalUsersRaw = Number(systemSummary?.platform?.total_users);
2826
+ const totalMessagesRaw = Number(systemSummary?.usage?.total_messages);
2827
+ const totalCommandsRaw = Number(systemSummary?.usage?.total_commands);
2828
+ const httpLatencyP95Ms = Number(systemSummary?.observability?.http_latency_p95_ms);
2829
+ const lagP99Ms = Number(systemSummary?.observability?.lag_p99_ms);
2830
+ const resolvedLatencyMs = Number.isFinite(httpLatencyP95Ms) ? httpLatencyP95Ms : Number.isFinite(lagP99Ms) ? lagP99Ms : null;
2831
+
2832
+ const totalUsers = Number.isFinite(totalUsersRaw) ? Math.max(0, Math.round(totalUsersRaw)) : null;
2833
+ const totalMessages = Number.isFinite(totalMessagesRaw) ? Math.max(0, Math.round(totalMessagesRaw)) : null;
2834
+ const totalCommands = Number.isFinite(totalCommandsRaw) ? Math.max(0, Math.round(totalCommandsRaw)) : null;
2835
+ const systemLatencyMs = Number.isFinite(resolvedLatencyMs) ? Math.max(0, Number(resolvedLatencyMs.toFixed(2))) : null;
3382
2836
 
3383
2837
  return {
3384
- bots_online: botsOnline,
3385
- messages_today: messagesToday,
3386
- spam_blocked_today: spamBlockedToday,
3387
- uptime,
3388
- analytics_ok: analyticsError === null,
3389
- analytics_error: analyticsError,
2838
+ total_users: totalUsers,
2839
+ total_messages: totalMessages,
2840
+ total_commands: totalCommands,
2841
+ system_latency_ms: systemLatencyMs,
3390
2842
  updated_at: new Date().toISOString(),
3391
2843
  };
3392
2844
  };
@@ -5702,6 +5154,7 @@ const buildSystemSummarySnapshot = async () => {
5702
5154
  let prometheus = null;
5703
5155
  let prometheusError = null;
5704
5156
  let platformError = null;
5157
+ let usageError = null;
5705
5158
 
5706
5159
  const socketReadyState = resolveSocketReadyState(activeSocket);
5707
5160
  const botJid = resolveActiveSocketBotJid(activeSocket) || null;
@@ -5714,6 +5167,11 @@ const buildSystemSummarySnapshot = async () => {
5714
5167
  total_groups: null,
5715
5168
  total_chats: null,
5716
5169
  };
5170
+ let usage = {
5171
+ total_messages: null,
5172
+ total_commands: null,
5173
+ total_commands_source: TABLES.MESSAGE_ANALYSIS_EVENT,
5174
+ };
5717
5175
 
5718
5176
  try {
5719
5177
  prometheus = await fetchPrometheusSummary();
@@ -5750,6 +5208,27 @@ const buildSystemSummarySnapshot = async () => {
5750
5208
  platformError = error?.message || 'Falha ao consultar totais de usuários/grupos';
5751
5209
  }
5752
5210
 
5211
+ try {
5212
+ const [messageTotalsRows, commandTotalsRows] = await Promise.all([
5213
+ executeQuery(`SELECT COUNT(*) AS total_messages FROM ${TABLES.MESSAGES}`),
5214
+ executeQuery(
5215
+ `SELECT COUNT(*) AS total_commands
5216
+ FROM ${TABLES.MESSAGE_ANALYSIS_EVENT}
5217
+ WHERE is_command = 1
5218
+ AND COALESCE(is_from_bot, 0) = 0`,
5219
+ ),
5220
+ ]);
5221
+ const messageTotals = messageTotalsRows?.[0] || {};
5222
+ const commandTotals = commandTotalsRows?.[0] || {};
5223
+ usage = {
5224
+ ...usage,
5225
+ total_messages: Number(messageTotals?.total_messages || 0),
5226
+ total_commands: Number(commandTotals?.total_commands || 0),
5227
+ };
5228
+ } catch (error) {
5229
+ usageError = error?.message || 'Falha ao consultar totais de mensagens/comandos';
5230
+ }
5231
+
5753
5232
  const hostCpuPercent = Number(system.usoCpuPercentual);
5754
5233
  const hostMemoryPercent = Number(system.usoMemoriaPercentual);
5755
5234
  const statusReasons = [];
@@ -5771,6 +5250,7 @@ const buildSystemSummarySnapshot = async () => {
5771
5250
  ready_state: socketReadyState,
5772
5251
  },
5773
5252
  platform,
5253
+ usage,
5774
5254
  host: {
5775
5255
  cpu_percent: system.usoCpuPercentual,
5776
5256
  memory_percent: system.usoMemoriaPercentual,
@@ -5797,6 +5277,7 @@ const buildSystemSummarySnapshot = async () => {
5797
5277
  metrics_ok: Boolean(prometheus),
5798
5278
  metrics_error: prometheusError,
5799
5279
  platform_error: platformError,
5280
+ usage_error: usageError,
5800
5281
  },
5801
5282
  };
5802
5283
  };
@@ -6719,1512 +6200,44 @@ const handlePackInteractionRequest = async (req, res, packKey, interaction, url)
6719
6200
  });
6720
6201
  };
6721
6202
 
6722
- const safeParseJsonObject = (value) => {
6723
- if (!value) return null;
6724
- if (typeof value === 'object') return value;
6725
- try {
6726
- const parsed = JSON.parse(String(value));
6727
- return parsed && typeof parsed === 'object' ? parsed : null;
6728
- } catch {
6729
- return null;
6730
- }
6731
- };
6732
-
6733
- const sanitizeAuditActionText = (value, max = 96) =>
6734
- String(value || '')
6735
- .trim()
6736
- .toLowerCase()
6737
- .replace(/[^a-z0-9_:-]/g, '_')
6738
- .slice(0, max);
6739
-
6740
- const createAdminActionAuditEvent = async ({ adminSession = null, action = '', targetType = '', targetId = '', status = 'success', details = null } = {}) => {
6741
- const normalizedAction = sanitizeAuditActionText(action, 96);
6742
- if (!normalizedAction) return false;
6743
- const normalizedTargetType = sanitizeAuditActionText(targetType, 64) || null;
6744
- const normalizedStatus = sanitizeAuditActionText(status, 32) || 'success';
6745
- const detailsJson = details && typeof details === 'object' ? JSON.stringify(details) : null;
6746
- const adminRole = normalizeAdminPanelRole(adminSession?.role, 'owner');
6747
- const adminGoogleSub = normalizeGoogleSubject(adminSession?.googleSub) || null;
6748
- const adminEmail = normalizeEmail(adminSession?.email) || null;
6749
- const adminOwnerJid = normalizeJid(adminSession?.ownerJid) || null;
6750
-
6751
- try {
6752
- await executeQuery(
6753
- `INSERT INTO ${TABLES.ADMIN_ACTION_AUDIT}
6754
- (
6755
- id,
6756
- admin_role,
6757
- admin_google_sub,
6758
- admin_email,
6759
- admin_owner_jid,
6760
- action,
6761
- target_type,
6762
- target_id,
6763
- status,
6764
- details
6765
- )
6766
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
6767
- [randomUUID(), adminRole, adminGoogleSub, adminEmail, adminOwnerJid, normalizedAction, normalizedTargetType, sanitizeText(targetId || '', 255, { allowEmpty: true }) || null, normalizedStatus, detailsJson],
6768
- );
6769
- return true;
6770
- } catch (error) {
6771
- if (error?.code === 'ER_NO_SUCH_TABLE') return false;
6772
- logger.warn('Falha ao registrar auditoria admin.', {
6773
- action: 'admin_audit_insert_failed',
6774
- error: error?.message,
6775
- audit_action: normalizedAction,
6776
- });
6777
- return false;
6778
- }
6779
- };
6780
-
6781
- const listAdminAuditLog = async ({ limit = 80 } = {}) => {
6782
- const safeLimit = Math.max(1, Math.min(500, Number(limit || 80)));
6783
- try {
6784
- const rows = await executeQuery(
6785
- `SELECT
6786
- id,
6787
- admin_role,
6788
- admin_google_sub,
6789
- admin_email,
6790
- admin_owner_jid,
6791
- action,
6792
- target_type,
6793
- target_id,
6794
- status,
6795
- details,
6796
- created_at
6797
- FROM ${TABLES.ADMIN_ACTION_AUDIT}
6798
- ORDER BY created_at DESC
6799
- LIMIT ${safeLimit}`,
6800
- );
6801
-
6802
- return (Array.isArray(rows) ? rows : []).map((row) => ({
6803
- id: String(row?.id || '').trim(),
6804
- admin_role: normalizeAdminPanelRole(row?.admin_role, 'owner'),
6805
- admin_google_sub: normalizeGoogleSubject(row?.admin_google_sub),
6806
- admin_email: normalizeEmail(row?.admin_email) || null,
6807
- admin_owner_jid: normalizeJid(row?.admin_owner_jid) || null,
6808
- action: String(row?.action || '').trim(),
6809
- target_type: String(row?.target_type || '').trim() || null,
6810
- target_id: String(row?.target_id || '').trim() || null,
6811
- status: String(row?.status || '').trim() || 'success',
6812
- details: safeParseJsonObject(row?.details),
6813
- created_at: toIsoOrNull(row?.created_at),
6814
- }));
6815
- } catch (error) {
6816
- if (error?.code === 'ER_NO_SUCH_TABLE') return [];
6817
- throw error;
6818
- }
6819
- };
6820
-
6821
- const listAdminFeatureFlagsDetailed = async ({ limit = 300 } = {}) => {
6822
- const safeLimit = Math.max(1, Math.min(500, Number(limit || 300)));
6823
- try {
6824
- const rows = await executeQuery(
6825
- `SELECT
6826
- flag_name,
6827
- is_enabled,
6828
- rollout_percent,
6829
- description,
6830
- updated_by,
6831
- updated_at
6832
- FROM ${TABLES.FEATURE_FLAG}
6833
- ORDER BY flag_name ASC
6834
- LIMIT ${safeLimit}`,
6835
- );
6836
- return (Array.isArray(rows) ? rows : []).map((row) => ({
6837
- flag_name: sanitizeText(row?.flag_name || '', 120, { allowEmpty: false }) || '',
6838
- is_enabled: Number(row?.is_enabled || 0) === 1,
6839
- rollout_percent: Math.max(0, Math.min(100, Number(row?.rollout_percent || 0))),
6840
- description: sanitizeText(row?.description || '', 255, { allowEmpty: true }) || null,
6841
- updated_by: sanitizeText(row?.updated_by || '', 120, { allowEmpty: true }) || null,
6842
- updated_at: toIsoOrNull(row?.updated_at),
6843
- }));
6844
- } catch (error) {
6845
- if (error?.code === 'ER_NO_SUCH_TABLE') {
6846
- const fallback = await getFeatureFlagsSnapshot().catch(() => []);
6847
- return (Array.isArray(fallback) ? fallback : []).map((entry) => ({
6848
- flag_name: sanitizeText(entry?.flag_name || '', 120, { allowEmpty: false }) || '',
6849
- is_enabled: Boolean(entry?.is_enabled),
6850
- rollout_percent: Math.max(0, Math.min(100, Number(entry?.rollout_percent || 0))),
6851
- description: null,
6852
- updated_by: null,
6853
- updated_at: null,
6854
- }));
6855
- }
6856
- throw error;
6857
- }
6858
- };
6859
-
6860
- const upsertAdminFeatureFlagRecord = async ({ adminSession = null, flagName = '', isEnabled = false, rolloutPercent = 100, description = '' } = {}) => {
6861
- const normalizedFlagName = sanitizeAuditActionText(flagName, 120);
6862
- if (!normalizedFlagName) {
6863
- const error = new Error('flag_name invalido.');
6864
- error.statusCode = 400;
6865
- throw error;
6866
- }
6867
- const normalizedRollout = Math.max(0, Math.min(100, Math.floor(Number(rolloutPercent) || 0)));
6868
- const normalizedEnabled = isEnabled ? 1 : 0;
6869
- const normalizedDescription = sanitizeText(description || '', 255, { allowEmpty: true }) || null;
6870
- const updatedBy = normalizeEmail(adminSession?.email) || normalizeGoogleSubject(adminSession?.googleSub) || 'admin';
6871
-
6872
- await executeQuery(
6873
- `INSERT INTO ${TABLES.FEATURE_FLAG}
6874
- (flag_name, is_enabled, rollout_percent, description, updated_by)
6875
- VALUES (?, ?, ?, ?, ?)
6876
- ON DUPLICATE KEY UPDATE
6877
- is_enabled = VALUES(is_enabled),
6878
- rollout_percent = VALUES(rollout_percent),
6879
- description = COALESCE(VALUES(description), description),
6880
- updated_by = VALUES(updated_by),
6881
- updated_at = CURRENT_TIMESTAMP`,
6882
- [normalizedFlagName, normalizedEnabled, normalizedRollout, normalizedDescription, sanitizeText(updatedBy, 120, { allowEmpty: true }) || null],
6883
- );
6884
-
6885
- await refreshFeatureFlags({ force: true }).catch(() => {});
6886
- const rows = await executeQuery(
6887
- `SELECT flag_name, is_enabled, rollout_percent, description, updated_by, updated_at
6888
- FROM ${TABLES.FEATURE_FLAG}
6889
- WHERE flag_name = ?
6890
- LIMIT 1`,
6891
- [normalizedFlagName],
6892
- );
6893
- const row = Array.isArray(rows) ? rows[0] : null;
6894
- return {
6895
- flag_name: sanitizeText(row?.flag_name || normalizedFlagName, 120, { allowEmpty: false }) || normalizedFlagName,
6896
- is_enabled: Number(row?.is_enabled || 0) === 1,
6897
- rollout_percent: Math.max(0, Math.min(100, Number(row?.rollout_percent ?? normalizedRollout))),
6898
- description: sanitizeText(row?.description || '', 255, { allowEmpty: true }) || null,
6899
- updated_by: sanitizeText(row?.updated_by || '', 120, { allowEmpty: true }) || null,
6900
- updated_at: toIsoOrNull(row?.updated_at) || new Date().toISOString(),
6901
- };
6902
- };
6903
-
6904
- const getAdminMessageFlowDailyStats = async () => {
6905
- try {
6906
- const [row] = await executeQuery(
6907
- `SELECT
6908
- COUNT(*) AS messages_today,
6909
- SUM(CASE WHEN processing_result = 'blocked_antilink' THEN 1 ELSE 0 END) AS spam_blocked_today,
6910
- SUM(CASE WHEN processing_result = 'auth_required' THEN 1 ELSE 0 END) AS suspicious_today
6911
- FROM ${TABLES.MESSAGE_ANALYSIS_EVENT}
6912
- WHERE created_at >= UTC_DATE()`,
6913
- );
6914
- return {
6915
- messages_today: Number(row?.messages_today || 0),
6916
- spam_blocked_today: Number(row?.spam_blocked_today || 0),
6917
- suspicious_today: Number(row?.suspicious_today || 0),
6918
- available: true,
6919
- };
6920
- } catch (error) {
6921
- if (error?.code === 'ER_NO_SUCH_TABLE') {
6922
- return {
6923
- messages_today: null,
6924
- spam_blocked_today: null,
6925
- suspicious_today: null,
6926
- available: false,
6927
- };
6928
- }
6929
- throw error;
6930
- }
6931
- };
6932
-
6933
- const listRecentModerationEvents = async ({ limit = 40 } = {}) => {
6934
- const safeLimit = Math.max(1, Math.min(200, Number(limit || 40)));
6935
- try {
6936
- const rows = await executeQuery(
6937
- `SELECT
6938
- id,
6939
- message_id,
6940
- chat_id,
6941
- sender_id,
6942
- sender_name,
6943
- processing_result,
6944
- command_name,
6945
- error_code,
6946
- metadata,
6947
- created_at
6948
- FROM ${TABLES.MESSAGE_ANALYSIS_EVENT}
6949
- WHERE processing_result IN ('blocked_antilink', 'auth_required')
6950
- ORDER BY created_at DESC
6951
- LIMIT ${safeLimit}`,
6952
- );
6953
-
6954
- return (Array.isArray(rows) ? rows : []).map((row) => {
6955
- const processingResult = String(row?.processing_result || '')
6956
- .trim()
6957
- .toLowerCase();
6958
- const metadata = safeParseJsonObject(row?.metadata);
6959
- const isAntiLink = processingResult === 'blocked_antilink';
6960
- const title = isAntiLink ? 'Anti-link bloqueou mensagem' : 'Tentativa suspeita detectada';
6961
- const severity = isAntiLink ? 'medium' : 'high';
6962
- const sender = sanitizeText(row?.sender_name || row?.sender_id || '', 120, { allowEmpty: true }) || String(row?.sender_id || '').trim() || 'desconhecido';
6963
- const chatId = String(row?.chat_id || '').trim() || 'chat_desconhecido';
6964
- return {
6965
- id: `mae:${row?.id || ''}`,
6966
- event_type: isAntiLink ? 'anti_link' : 'suspicious',
6967
- severity,
6968
- title,
6969
- subtitle: `${sender} em ${chatId}`,
6970
- chat_id: chatId,
6971
- sender_id: String(row?.sender_id || '').trim() || null,
6972
- sender_name: sanitizeText(row?.sender_name || '', 120, { allowEmpty: true }) || null,
6973
- message_id: String(row?.message_id || '').trim() || null,
6974
- processing_result: processingResult,
6975
- command_name: sanitizeText(row?.command_name || '', 64, { allowEmpty: true }) || null,
6976
- error_code: sanitizeText(row?.error_code || '', 96, { allowEmpty: true }) || null,
6977
- metadata,
6978
- created_at: toIsoOrNull(row?.created_at),
6979
- };
6980
- });
6981
- } catch (error) {
6982
- if (error?.code === 'ER_NO_SUCH_TABLE') return [];
6983
- throw error;
6984
- }
6985
- };
6986
-
6987
- const buildModerationQueueSnapshot = async ({ limit = 50 } = {}) => {
6988
- const [analysisEvents, bans] = await Promise.all([listRecentModerationEvents({ limit: Math.max(10, limit) }), listAdminBans({ activeOnly: false, limit: Math.max(10, Math.floor(limit / 2)) })]);
6989
-
6990
- const banEvents = (Array.isArray(bans) ? bans : []).map((ban) => ({
6991
- id: `ban:${ban?.id || ''}`,
6992
- event_type: 'ban',
6993
- severity: ban?.revoked_at ? 'low' : 'critical',
6994
- title: ban?.revoked_at ? 'Ban revogado' : 'Conta bloqueada',
6995
- subtitle: sanitizeText(ban?.email || ban?.owner_jid || ban?.google_sub || '', 160, { allowEmpty: true }) || 'identidade indisponivel',
6996
- ban_id: String(ban?.id || '').trim(),
6997
- reason: sanitizeText(ban?.reason || '', 255, { allowEmpty: true }) || null,
6998
- created_at: toIsoOrNull(ban?.created_at),
6999
- revoked_at: toIsoOrNull(ban?.revoked_at),
7000
- metadata: {
7001
- google_sub: ban?.google_sub || null,
7002
- email: ban?.email || null,
7003
- owner_jid: ban?.owner_jid || null,
7004
- },
7005
- }));
7006
-
7007
- const combined = [...(Array.isArray(analysisEvents) ? analysisEvents : []), ...banEvents];
7008
- combined.sort((left, right) => {
7009
- const leftTs = Date.parse(String(left?.created_at || left?.revoked_at || 0)) || 0;
7010
- const rightTs = Date.parse(String(right?.created_at || right?.revoked_at || 0)) || 0;
7011
- return rightTs - leftTs;
7012
- });
7013
- return combined.slice(0, Math.max(1, Math.min(200, Number(limit || 50))));
7014
- };
7015
-
7016
- const buildAdminSystemHealthSnapshot = ({ systemSummary = null, systemMeta = null } = {}) => {
7017
- const hostCpu = Number(systemSummary?.host?.cpu_percent);
7018
- const hostRam = Number(systemSummary?.host?.memory_percent);
7019
- const latencyP95 = Number(systemSummary?.observability?.http_latency_p95_ms);
7020
- const queuePending = Number(systemSummary?.observability?.queue_peak);
7021
- const hasMetricsError = Boolean(systemMeta?.metrics_error);
7022
- const hasPlatformError = Boolean(systemMeta?.platform_error);
7023
- const dbStatus = hasPlatformError ? 'degraded' : hasMetricsError ? 'unknown' : 'ok';
7024
-
7025
- return {
7026
- cpu_percent: Number.isFinite(hostCpu) ? hostCpu : null,
7027
- ram_percent: Number.isFinite(hostRam) ? hostRam : null,
7028
- http_latency_p95_ms: Number.isFinite(latencyP95) ? latencyP95 : null,
7029
- queue_pending: Number.isFinite(queuePending) ? queuePending : null,
7030
- db_status: dbStatus,
7031
- db_total_queries: Number(systemSummary?.observability?.db_total ?? 0) || 0,
7032
- db_slow_queries: Number(systemSummary?.observability?.db_slow ?? 0) || 0,
7033
- bot_status: String(systemSummary?.bot?.connection_status || '').trim() || 'unknown',
7034
- updated_at: toIsoOrNull(systemSummary?.updated_at),
7035
- };
7036
- };
7037
-
7038
- const buildAdminAlertSnapshot = ({ dashboardQuick = null, systemHealth = null, systemSummary = null, systemMeta = null } = {}) => {
7039
- const alerts = [];
7040
- const updatedAt = toIsoOrNull(systemSummary?.updated_at) || new Date().toISOString();
7041
- const pushAlert = (severity, code, title, message) => {
7042
- alerts.push({
7043
- id: `${code}:${severity}`,
7044
- severity,
7045
- code,
7046
- title,
7047
- message,
7048
- created_at: updatedAt,
7049
- });
7050
- };
7051
-
7052
- const botStatus = String(systemSummary?.bot?.connection_status || '').toLowerCase();
7053
- if (botStatus && botStatus !== 'online') {
7054
- pushAlert('critical', 'bot_offline', 'Bot fora do ar', `Status atual: ${botStatus}.`);
7055
- }
7056
-
7057
- if (Number.isFinite(systemHealth?.cpu_percent) && systemHealth.cpu_percent >= 90) {
7058
- pushAlert('high', 'cpu_high', 'CPU alta', `Uso de CPU em ${systemHealth.cpu_percent.toFixed(1)}%.`);
7059
- }
7060
- if (Number.isFinite(systemHealth?.ram_percent) && systemHealth.ram_percent >= 90) {
7061
- pushAlert('high', 'ram_high', 'RAM alta', `Uso de RAM em ${systemHealth.ram_percent.toFixed(1)}%.`);
7062
- }
7063
- if (Number.isFinite(systemHealth?.queue_pending) && systemHealth.queue_pending >= 100) {
7064
- pushAlert('medium', 'queue_high', 'Fila pendente alta', `Backlog detectado (${Math.round(systemHealth.queue_pending)}).`);
7065
- }
7066
- if (Number.isFinite(dashboardQuick?.errors_5xx) && dashboardQuick.errors_5xx > 0) {
7067
- pushAlert('medium', 'http_5xx', 'Erros HTTP 5xx detectados', `${Math.round(dashboardQuick.errors_5xx)} eventos 5xx desde o boot de métricas.`);
7068
- }
7069
- if (systemMeta?.platform_error) {
7070
- pushAlert('high', 'db_platform_error', 'Erro de banco/plataforma', String(systemMeta.platform_error).slice(0, 200));
7071
- }
7072
- if (systemMeta?.metrics_error) {
7073
- pushAlert('low', 'metrics_unavailable', 'Métricas indisponíveis', String(systemMeta.metrics_error).slice(0, 200));
7074
- }
7075
-
7076
- return alerts;
7077
- };
7078
-
7079
- const listAdminActiveGoogleWebSessions = async ({ limit = 200 } = {}) => {
7080
- const safeLimit = Math.max(1, Math.min(500, Number(limit || 200)));
7081
- const rows = await executeQuery(
7082
- `SELECT session_token, google_sub, owner_jid, owner_phone, email, name, picture_url, created_at, last_seen_at, expires_at
7083
- FROM ${TABLES.STICKER_WEB_GOOGLE_SESSION}
7084
- WHERE revoked_at IS NULL
7085
- AND expires_at > UTC_TIMESTAMP()
7086
- ORDER BY COALESCE(last_seen_at, created_at) DESC
7087
- LIMIT ${safeLimit}`,
7088
- );
7089
- return (Array.isArray(rows) ? rows : []).map((row) => ({
7090
- session_token: String(row.session_token || '').trim(),
7091
- google_sub: normalizeGoogleSubject(row.google_sub),
7092
- owner_jid: normalizeJid(row.owner_jid) || null,
7093
- owner_phone: toWhatsAppPhoneDigits(row.owner_phone || row.owner_jid) || null,
7094
- email: normalizeEmail(row.email) || null,
7095
- name: sanitizeText(row.name || '', 120, { allowEmpty: true }) || null,
7096
- picture: String(row.picture_url || '').trim() || null,
7097
- created_at: toIsoOrNull(row.created_at),
7098
- last_seen_at: toIsoOrNull(row.last_seen_at),
7099
- expires_at: toIsoOrNull(row.expires_at),
7100
- }));
7101
- };
7102
-
7103
- const listAdminKnownGoogleUsers = async ({ limit = 200 } = {}) => {
7104
- const safeLimit = Math.max(1, Math.min(500, Number(limit || 200)));
7105
- const rows = await executeQuery(
7106
- `SELECT google_sub, owner_jid, owner_phone, email, name, picture_url, created_at, updated_at, last_login_at, last_seen_at
7107
- FROM ${TABLES.STICKER_WEB_GOOGLE_USER}
7108
- ORDER BY COALESCE(last_seen_at, last_login_at, updated_at, created_at) DESC
7109
- LIMIT ${safeLimit}`,
7110
- );
7111
- return (Array.isArray(rows) ? rows : []).map((row) => ({
7112
- google_sub: normalizeGoogleSubject(row.google_sub),
7113
- owner_jid: normalizeJid(row.owner_jid) || null,
7114
- owner_phone: toWhatsAppPhoneDigits(row.owner_phone || row.owner_jid) || null,
7115
- email: normalizeEmail(row.email) || null,
7116
- name: sanitizeText(row.name || '', 120, { allowEmpty: true }) || null,
7117
- picture: String(row.picture_url || '').trim() || null,
7118
- created_at: toIsoOrNull(row.created_at),
7119
- updated_at: toIsoOrNull(row.updated_at),
7120
- last_login_at: toIsoOrNull(row.last_login_at),
7121
- last_seen_at: toIsoOrNull(row.last_seen_at),
7122
- }));
7123
- };
7124
-
7125
- const getWebVisitSummary = async ({ rangeDays = 7, topPathsLimit = 10 } = {}) => {
7126
- const safeRangeDays = Math.max(1, Math.min(90, Number(rangeDays || 7)));
7127
- const safeTopPathsLimit = Math.max(1, Math.min(30, Number(topPathsLimit || 10)));
7128
-
7129
- try {
7130
- const [countersRows, topPathsRows] = await Promise.all([
7131
- executeQuery(
7132
- `SELECT
7133
- COUNT(*) AS total_events,
7134
- SUM(CASE WHEN created_at >= (UTC_TIMESTAMP() - INTERVAL 1 DAY) THEN 1 ELSE 0 END) AS events_24h,
7135
- SUM(CASE WHEN created_at >= (UTC_TIMESTAMP() - INTERVAL ${safeRangeDays} DAY) THEN 1 ELSE 0 END) AS events_range,
7136
- COUNT(DISTINCT CASE WHEN created_at >= (UTC_TIMESTAMP() - INTERVAL ${safeRangeDays} DAY) THEN visitor_key END) AS unique_visitors_range,
7137
- COUNT(DISTINCT CASE WHEN created_at >= (UTC_TIMESTAMP() - INTERVAL ${safeRangeDays} DAY) THEN session_key END) AS unique_sessions_range
7138
- FROM ${TABLES.WEB_VISIT_EVENT}`,
7139
- ),
7140
- executeQuery(
7141
- `SELECT page_path, COUNT(*) AS total
7142
- FROM ${TABLES.WEB_VISIT_EVENT}
7143
- WHERE created_at >= (UTC_TIMESTAMP() - INTERVAL ${safeRangeDays} DAY)
7144
- GROUP BY page_path
7145
- ORDER BY total DESC
7146
- LIMIT ${safeTopPathsLimit}`,
7147
- ),
7148
- ]);
7149
-
7150
- const counters = Array.isArray(countersRows) ? countersRows[0] || {} : {};
7151
- const topPaths = (Array.isArray(topPathsRows) ? topPathsRows : []).map((row) => ({
7152
- page_path: normalizeVisitPath(row?.page_path || '/'),
7153
- total: Number(row?.total || 0),
7154
- }));
7155
-
7156
- return {
7157
- range_days: safeRangeDays,
7158
- total_events: Number(counters?.total_events || 0),
7159
- events_24h: Number(counters?.events_24h || 0),
7160
- events_range: Number(counters?.events_range || 0),
7161
- unique_visitors_range: Number(counters?.unique_visitors_range || 0),
7162
- unique_sessions_range: Number(counters?.unique_sessions_range || 0),
7163
- top_paths: topPaths,
7164
- };
7165
- } catch (error) {
7166
- if (error?.code === 'ER_NO_SUCH_TABLE') return null;
7167
- throw error;
7168
- }
7169
- };
7170
-
7171
- const listAdminPacks = async (url) => {
7172
- const q = sanitizeText(url?.searchParams?.get('q') || '', 120, { allowEmpty: true }) || '';
7173
- const owner = normalizeJid(url?.searchParams?.get('owner_jid') || '') || '';
7174
- const limit = Math.max(1, Math.min(200, Number(url?.searchParams?.get('limit') || 50)));
7175
- const params = [];
7176
- const where = ['p.deleted_at IS NULL'];
7177
- if (q) {
7178
- where.push('(p.pack_key LIKE ? OR p.name LIKE ? OR p.publisher LIKE ? OR p.owner_jid LIKE ?)');
7179
- const like = `%${q}%`;
7180
- params.push(like, like, like, like);
7181
- }
7182
- if (owner) {
7183
- where.push('p.owner_jid = ?');
7184
- params.push(owner);
7185
- }
7186
- const rows = await executeQuery(
7187
- `SELECT
7188
- p.id,
7189
- p.pack_key,
7190
- p.owner_jid,
7191
- p.name,
7192
- p.publisher,
7193
- p.visibility,
7194
- p.status,
7195
- p.pack_status,
7196
- p.is_auto_pack,
7197
- p.pack_theme_key,
7198
- p.pack_volume,
7199
- p.created_at,
7200
- p.updated_at,
7201
- p.cover_sticker_id,
7202
- (SELECT COUNT(*) FROM ${TABLES.STICKER_PACK_ITEM} i WHERE i.pack_id = p.id) AS stickers_count,
7203
- COALESCE(e.open_count, 0) AS open_count,
7204
- COALESCE(e.like_count, 0) AS like_count,
7205
- COALESCE(e.dislike_count, 0) AS dislike_count
7206
- FROM ${TABLES.STICKER_PACK} p
7207
- LEFT JOIN ${TABLES.STICKER_PACK_ENGAGEMENT} e ON e.pack_id = p.id
7208
- WHERE ${where.join(' AND ')}
7209
- ORDER BY p.updated_at DESC
7210
- LIMIT ${limit}`,
7211
- params,
7212
- );
7213
- return (Array.isArray(rows) ? rows : []).map((row) => ({
7214
- id: String(row.id || ''),
7215
- pack_key: String(row.pack_key || ''),
7216
- owner_jid: normalizeJid(row.owner_jid) || null,
7217
- name: String(row.name || ''),
7218
- publisher: String(row.publisher || ''),
7219
- visibility: String(row.visibility || ''),
7220
- status: String(row.status || ''),
7221
- pack_status: String(row.pack_status || 'ready'),
7222
- is_auto_pack: Boolean(Number(row.is_auto_pack || 0)),
7223
- pack_theme_key: String(row.pack_theme_key || '').trim() || null,
7224
- pack_volume: Number.isFinite(Number(row.pack_volume)) ? Number(row.pack_volume) : null,
7225
- created_at: toIsoOrNull(row.created_at),
7226
- updated_at: toIsoOrNull(row.updated_at),
7227
- cover_sticker_id: String(row.cover_sticker_id || '').trim() || null,
7228
- stickers_count: Number(row.stickers_count || 0),
7229
- open_count: Number(row.open_count || 0),
7230
- like_count: Number(row.like_count || 0),
7231
- dislike_count: Number(row.dislike_count || 0),
7232
- web_url: `${STICKER_WEB_PATH}/${encodeURIComponent(String(row.pack_key || ''))}`,
7233
- }));
7234
- };
7235
-
7236
- const buildAdminOverviewPayload = async ({ adminSession = null } = {}) => {
7237
- 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(() => [])]);
7238
-
7239
- const systemSummary = systemSummaryPayload?.data || null;
7240
- const systemMeta = systemSummaryPayload?.meta || null;
7241
- const botsOnline = systemSummary?.bot?.connected ? 1 : 0;
7242
- const errors5xx = Number(systemSummary?.observability?.http_5xx_total ?? 0);
7243
- const dashboardQuick = {
7244
- bots_online: botsOnline,
7245
- messages_today: Number(messageFlowDaily?.messages_today ?? 0),
7246
- spam_blocked_today: Number(messageFlowDaily?.spam_blocked_today ?? 0),
7247
- uptime: String(systemSummary?.process?.uptime || '').trim() || 'n/d',
7248
- errors_5xx: Number.isFinite(errors5xx) ? Math.max(0, errors5xx) : 0,
7249
- };
7250
- const systemHealth = buildAdminSystemHealthSnapshot({ systemSummary, systemMeta });
7251
- const alerts = buildAdminAlertSnapshot({ dashboardQuick, systemHealth, systemSummary, systemMeta });
7252
-
7253
- return {
7254
- admin_session: mapAdminPanelSessionResponseData(adminSession),
7255
- marketplace_stats: marketplaceStats,
7256
- counters: {
7257
- total_packs_any_status: Number(packsCountRows?.[0]?.total || 0),
7258
- total_stickers_any_status: Number(stickersCountRows?.[0]?.total || 0),
7259
- active_google_sessions: Number(activeSessions.length || 0),
7260
- known_google_users: Number(knownUsers.length || 0),
7261
- active_bans: Number(bans.length || 0),
7262
- visit_events_24h: Number(visitSummary?.events_24h || 0),
7263
- visit_events_7d: Number(visitSummary?.events_range || 0),
7264
- unique_visitors_7d: Number(visitSummary?.unique_visitors_range || 0),
7265
- },
7266
- dashboard_quick: dashboardQuick,
7267
- moderation_queue: moderationQueue,
7268
- users_sessions: {
7269
- active_sessions: activeSessions,
7270
- users: knownUsers,
7271
- blocked_accounts: bans,
7272
- },
7273
- system_health: systemHealth,
7274
- audit_log: auditLog,
7275
- feature_flags: featureFlags,
7276
- alerts,
7277
- operational_shortcuts: [
7278
- { action: 'restart_worker', label: 'Reiniciar worker', description: 'Destrava filas em processamento e recoloca em pending.' },
7279
- { action: 'clear_cache', label: 'Limpar cache', description: 'Invalida caches internos de catálogo, ranking e resumo.' },
7280
- { action: 'reprocess_jobs', label: 'Reprocessar jobs', description: 'Agenda ciclos de classificação/curadoria no worker.' },
7281
- ],
7282
- active_sessions: activeSessions,
7283
- users: knownUsers,
7284
- bans,
7285
- recent_packs: recentPacks,
7286
- visit_metrics: visitSummary,
7287
- system_summary: systemSummary,
7288
- system_meta: systemMeta,
7289
- message_flow_daily: messageFlowDaily,
7290
- updated_at: new Date().toISOString(),
7291
- };
7292
- };
7293
-
7294
- const findAdminPackContextByKey = async (rawPackKey) => {
7295
- const packKey = sanitizeText(rawPackKey, 160, { allowEmpty: false });
7296
- if (!packKey) return null;
7297
- const basePack = await findStickerPackByPackKey(packKey);
7298
- if (!basePack) return null;
7299
- const ownerJid = normalizeJid(basePack.owner_jid) || '';
7300
- if (!ownerJid) return null;
7301
- const fullPack = await stickerPackService.getPackInfo({ ownerJid, identifier: basePack.pack_key });
7302
- return { basePack, fullPack, ownerJid, packKey: basePack.pack_key };
7303
- };
7304
-
7305
- const handleAdminPanelSessionRequest = async (req, res) => {
7306
- if (!ADMIN_PANEL_ENABLED) {
7307
- sendJson(req, res, 404, { error: 'Painel admin desabilitado.' });
7308
- return;
7309
- }
7310
-
7311
- if (req.method === 'GET' || req.method === 'HEAD') {
7312
- const googleSession = await resolveGoogleWebSessionFromRequest(req);
7313
- const adminSession = resolveAdminPanelSessionFromRequest(req);
7314
- const eligibility = await resolveAdminPanelLoginEligibility(googleSession);
7315
- sendJson(req, res, 200, {
7316
- data: {
7317
- google: mapGoogleSessionResponseData(googleSession),
7318
- eligible_google_login: Boolean(eligibility.eligible),
7319
- eligible_role: eligibility.role || null,
7320
- session: mapAdminPanelSessionResponseData(adminSession),
7321
- },
7322
- });
7323
- return;
7324
- }
7325
-
7326
- if (req.method === 'DELETE') {
7327
- const token = getAdminPanelSessionTokenFromRequest(req);
7328
- const adminSession = resolveAdminPanelSessionFromRequest(req);
7329
- if (token) adminPanelSessionMap.delete(token);
7330
- clearAdminPanelSessionCookie(req, res);
7331
- await createAdminActionAuditEvent({
7332
- adminSession,
7333
- action: 'admin_session_logout',
7334
- targetType: 'admin_session',
7335
- targetId: token || 'cookie_clear',
7336
- });
7337
- sendJson(req, res, 200, { data: { cleared: true } });
7338
- return;
7339
- }
7340
-
7341
- if (req.method !== 'POST') {
7342
- sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
7343
- return;
7344
- }
7345
-
7346
- let payload = {};
7347
- try {
7348
- payload = await readJsonBody(req);
7349
- } catch (error) {
7350
- sendJson(req, res, Number(error?.statusCode || 400), { error: error?.message || 'Body inválido.' });
7351
- return;
7352
- }
7353
-
7354
- const googleSession = await resolveGoogleWebSessionFromRequest(req);
7355
- const eligibility = await resolveAdminPanelLoginEligibility(googleSession);
7356
- if (!eligibility.eligible) {
7357
- sendJson(req, res, 403, { error: 'Conta Google sem permissao para o painel admin.' });
7358
- return;
7359
- }
7360
- const password = String(payload?.password || '').trim();
7361
- let sessionRole = 'owner';
7362
- if (eligibility.role === 'owner') {
7363
- if (!password || !constantTimeStringEqual(password, ADMIN_PANEL_PASSWORD)) {
7364
- sendJson(req, res, 401, { error: 'Senha do painel admin invalida.' });
7365
- return;
7366
- }
7367
- sessionRole = 'owner';
7368
- } else if (eligibility.role === 'moderator') {
7369
- const moderatorHash = String(eligibility?.moderator?.password_hash || '').trim();
7370
- if (!password || !verifyAdminModeratorPassword(password, moderatorHash)) {
7371
- sendJson(req, res, 401, { error: 'Senha do moderador invalida.' });
7372
- return;
7373
- }
7374
- sessionRole = 'moderator';
7375
- await touchAdminModeratorLastLogin(eligibility.moderator, googleSession).catch(() => {});
7376
- } else {
7377
- sendJson(req, res, 403, { error: 'Conta Google sem permissao para o painel admin.' });
7378
- return;
7379
- }
7380
-
7381
- const session = createAdminPanelSession(googleSession, { role: sessionRole });
7382
- appendSetCookie(
7383
- res,
7384
- buildCookieString(ADMIN_PANEL_SESSION_COOKIE_NAME, session.token, req, {
7385
- maxAgeSeconds: Math.floor(ADMIN_PANEL_SESSION_TTL_MS / 1000),
7386
- }),
7387
- );
7388
- sendJson(req, res, 200, {
7389
- data: {
7390
- google: mapGoogleSessionResponseData(googleSession),
7391
- eligible_google_login: true,
7392
- eligible_role: sessionRole,
7393
- session: mapAdminPanelSessionResponseData(session),
7394
- },
7395
- });
7396
- await createAdminActionAuditEvent({
7397
- adminSession: session,
7398
- action: 'admin_session_login',
7399
- targetType: 'admin_session',
7400
- targetId: session.token,
7401
- details: { role: sessionRole },
7402
- });
7403
- };
7404
-
7405
- const handleAdminOverviewRequest = async (req, res) => {
7406
- const adminSession = requireAdminPanelSession(req, res);
7407
- if (!adminSession) return;
7408
- if (!['GET', 'HEAD'].includes(req.method || '')) {
7409
- sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
7410
- return;
7411
- }
7412
-
7413
- const overview = await buildAdminOverviewPayload({ adminSession });
7414
- sendJson(req, res, 200, { data: overview });
7415
- };
7416
-
7417
- const handleAdminUsersRequest = async (req, res, url) => {
7418
- const adminSession = requireAdminPanelSession(req, res);
7419
- if (!adminSession) return;
7420
- if (!['GET', 'HEAD'].includes(req.method || '')) {
7421
- sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
7422
- return;
7423
- }
7424
- const limit = Math.max(1, Math.min(500, Number(url?.searchParams?.get('limit') || 200)));
7425
- const [activeSessions, users] = await Promise.all([listAdminActiveGoogleWebSessions({ limit }), listAdminKnownGoogleUsers({ limit })]);
7426
- sendJson(req, res, 200, { data: { active_sessions: activeSessions, users } });
7427
- };
7428
-
7429
- const handleAdminForceLogoutRequest = async (req, res) => {
7430
- const adminSession = requireAdminPanelSession(req, res);
7431
- if (!adminSession) return;
7432
- if (req.method !== 'POST') {
7433
- sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
7434
- return;
7435
- }
7436
-
7437
- let payload = {};
7438
- try {
7439
- payload = await readJsonBody(req);
7440
- } catch (error) {
7441
- sendJson(req, res, Number(error?.statusCode || 400), { error: error?.message || 'Body inválido.' });
7442
- return;
7443
- }
7444
-
7445
- let googleSub = normalizeGoogleSubject(payload?.google_sub || '');
7446
- let email = normalizeEmail(payload?.email || '');
7447
- let ownerJid = normalizeJid(payload?.owner_jid || '') || '';
7448
- const sessionToken = sanitizeText(payload?.session_token || '', 36, { allowEmpty: true }) || '';
7449
-
7450
- if (sessionToken) {
7451
- const rows = await executeQuery(
7452
- `SELECT google_sub, email, owner_jid
7453
- FROM ${TABLES.STICKER_WEB_GOOGLE_SESSION}
7454
- WHERE session_token = ?
7455
- LIMIT 1`,
7456
- [sessionToken],
7457
- ).catch(() => []);
7458
- const row = Array.isArray(rows) ? rows[0] : null;
7459
- if (row) {
7460
- googleSub = normalizeGoogleSubject(row.google_sub || googleSub);
7461
- email = normalizeEmail(row.email || email);
7462
- ownerJid = normalizeJid(row.owner_jid || ownerJid) || ownerJid;
7463
- }
7464
- }
7465
-
7466
- if (!googleSub && !email && !ownerJid) {
7467
- sendJson(req, res, 400, { error: 'Informe session_token, google_sub, email ou owner_jid.' });
7468
- return;
7469
- }
7470
-
7471
- const removed = await revokeGoogleWebSessionsByIdentity({
7472
- googleSub,
7473
- email,
7474
- ownerJid,
7475
- }).catch(() => 0);
7476
-
7477
- await createAdminActionAuditEvent({
7478
- adminSession,
7479
- action: 'force_logout',
7480
- targetType: 'google_web_session',
7481
- targetId: sessionToken || googleSub || email || ownerJid,
7482
- details: { removed_sessions: Number(removed || 0), google_sub: googleSub || null, email: email || null, owner_jid: ownerJid || null },
7483
- });
7484
-
7485
- sendJson(req, res, 200, {
7486
- data: {
7487
- removed_sessions: Number(removed || 0),
7488
- target: {
7489
- session_token: sessionToken || null,
7490
- google_sub: googleSub || null,
7491
- email: email || null,
7492
- owner_jid: ownerJid || null,
7493
- },
7494
- },
7495
- });
7496
- };
7497
-
7498
- const handleAdminFeatureFlagsRequest = async (req, res) => {
7499
- const adminSession = requireAdminPanelSession(req, res);
7500
- if (!adminSession) return;
7501
-
7502
- if (req.method === 'GET' || req.method === 'HEAD') {
7503
- const flags = await listAdminFeatureFlagsDetailed({ limit: 400 }).catch(() => []);
7504
- sendJson(req, res, 200, { data: { flags } });
7505
- return;
7506
- }
7507
-
7508
- if (req.method !== 'POST') {
7509
- sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
7510
- return;
7511
- }
7512
-
7513
- let payload = {};
7514
- try {
7515
- payload = await readJsonBody(req);
7516
- } catch (error) {
7517
- sendJson(req, res, Number(error?.statusCode || 400), { error: error?.message || 'Body inválido.' });
7518
- return;
7519
- }
7520
-
7521
- try {
7522
- const flag = await upsertAdminFeatureFlagRecord({
7523
- adminSession,
7524
- flagName: payload?.flag_name,
7525
- isEnabled: Boolean(payload?.is_enabled),
7526
- rolloutPercent: payload?.rollout_percent,
7527
- description: payload?.description,
7528
- });
7529
- await createAdminActionAuditEvent({
7530
- adminSession,
7531
- action: 'feature_flag_update',
7532
- targetType: 'feature_flag',
7533
- targetId: flag.flag_name,
7534
- details: {
7535
- is_enabled: flag.is_enabled,
7536
- rollout_percent: flag.rollout_percent,
7537
- },
7538
- });
7539
- sendJson(req, res, 200, { data: { flag } });
7540
- } catch (error) {
7541
- sendJson(req, res, Number(error?.statusCode || 400), { error: error?.message || 'Falha ao atualizar feature flag.' });
7542
- }
7543
- };
7544
-
7545
- const handleAdminOpsActionRequest = async (req, res) => {
7546
- const adminSession = requireAdminPanelSession(req, res);
7547
- if (!adminSession) return;
7548
- if (req.method !== 'POST') {
7549
- sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
7550
- return;
7551
- }
7552
-
7553
- let payload = {};
7554
- try {
7555
- payload = await readJsonBody(req);
7556
- } catch (error) {
7557
- sendJson(req, res, Number(error?.statusCode || 400), { error: error?.message || 'Body inválido.' });
7558
- return;
7559
- }
7560
-
7561
- const action = sanitizeAuditActionText(payload?.action || '', 64);
7562
- if (!action) {
7563
- sendJson(req, res, 400, { error: 'Informe a ação operacional.' });
7564
- return;
7565
- }
7566
-
7567
- try {
7568
- if (action === 'clear_cache') {
7569
- invalidateStickerCatalogDerivedCaches();
7570
- await createAdminActionAuditEvent({
7571
- adminSession,
7572
- action: 'ops_clear_cache',
7573
- targetType: 'cache',
7574
- targetId: 'global',
7575
- });
7576
- sendJson(req, res, 200, { data: { action, success: true, message: 'Caches internos invalidados com sucesso.', updated_at: new Date().toISOString() } });
7577
- return;
7578
- }
7579
-
7580
- if (action === 'restart_worker') {
7581
- const [tasksResult, reprocessResult] = await Promise.all([
7582
- executeQuery(
7583
- `UPDATE ${TABLES.STICKER_WORKER_TASK_QUEUE}
7584
- SET status = 'pending',
7585
- worker_token = NULL,
7586
- locked_at = NULL,
7587
- updated_at = CURRENT_TIMESTAMP
7588
- WHERE status = 'processing'`,
7589
- ).catch(() => ({ affectedRows: 0 })),
7590
- executeQuery(
7591
- `UPDATE ${TABLES.STICKER_ASSET_REPROCESS_QUEUE}
7592
- SET status = 'pending',
7593
- worker_token = NULL,
7594
- locked_at = NULL,
7595
- updated_at = CURRENT_TIMESTAMP
7596
- WHERE status = 'processing'`,
7597
- ).catch(() => ({ affectedRows: 0 })),
7598
- ]);
7599
-
7600
- const released = Number(tasksResult?.affectedRows || 0) + Number(reprocessResult?.affectedRows || 0);
7601
- await createAdminActionAuditEvent({
7602
- adminSession,
7603
- action: 'ops_restart_worker',
7604
- targetType: 'worker',
7605
- targetId: 'queues',
7606
- details: {
7607
- released_tasks: released,
7608
- task_queue: Number(tasksResult?.affectedRows || 0),
7609
- reprocess_queue: Number(reprocessResult?.affectedRows || 0),
7610
- },
7611
- });
7612
- sendJson(req, res, 200, {
7613
- data: {
7614
- action,
7615
- success: true,
7616
- released_processing_items: released,
7617
- message: released > 0 ? 'Itens em processamento foram recolocados em pending.' : 'Nenhum item travado encontrado nas filas.',
7618
- updated_at: new Date().toISOString(),
7619
- },
7620
- });
7621
- return;
7622
- }
7623
-
7624
- if (action === 'reprocess_jobs') {
7625
- const payloadJson = JSON.stringify({
7626
- source: 'admin_panel',
7627
- requested_by: normalizeEmail(adminSession?.email) || normalizeGoogleSubject(adminSession?.googleSub) || 'admin',
7628
- requested_at: new Date().toISOString(),
7629
- });
7630
- await executeQuery(
7631
- `INSERT INTO ${TABLES.STICKER_WORKER_TASK_QUEUE}
7632
- (task_type, payload, priority, scheduled_at, status, max_attempts)
7633
- VALUES
7634
- ('classification_cycle', ?, 10, UTC_TIMESTAMP(), 'pending', 5),
7635
- ('curation_cycle', ?, 12, UTC_TIMESTAMP(), 'pending', 5)`,
7636
- [payloadJson, payloadJson],
7637
- );
7638
- await createAdminActionAuditEvent({
7639
- adminSession,
7640
- action: 'ops_reprocess_jobs',
7641
- targetType: 'worker',
7642
- targetId: 'classification_cycle,curation_cycle',
7643
- });
7644
- sendJson(req, res, 200, {
7645
- data: {
7646
- action,
7647
- success: true,
7648
- enqueued_tasks: 2,
7649
- message: 'Ciclos de classificação e curadoria foram agendados.',
7650
- updated_at: new Date().toISOString(),
7651
- },
7652
- });
7653
- return;
7654
- }
7655
-
7656
- sendJson(req, res, 400, { error: 'Ação operacional inválida.' });
7657
- } catch (error) {
7658
- sendJson(req, res, 500, { error: error?.message || 'Falha ao executar ação operacional.' });
7659
- }
7660
- };
7661
-
7662
- const handleAdminSearchRequest = async (req, res, url) => {
7663
- const adminSession = requireAdminPanelSession(req, res);
7664
- if (!adminSession) return;
7665
- if (!['GET', 'HEAD'].includes(req.method || '')) {
7666
- sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
7667
- return;
7668
- }
7669
-
7670
- const q = sanitizeText(url?.searchParams?.get('q') || '', 120, { allowEmpty: true }) || '';
7671
- const limit = Math.max(1, Math.min(30, Number(url?.searchParams?.get('limit') || 12)));
7672
- if (!q) {
7673
- sendJson(req, res, 200, {
7674
- data: {
7675
- q: '',
7676
- totals: { users: 0, sessions: 0, groups: 0, packs: 0 },
7677
- results: { users: [], sessions: [], groups: [], packs: [] },
7678
- },
7679
- });
7680
- return;
7681
- }
7682
-
7683
- const like = `%${q}%`;
7684
-
7685
- const [usersRows, sessionsRows, groupsRows, packs] = await Promise.all([
7686
- executeQuery(
7687
- `SELECT google_sub, email, name, owner_jid, owner_phone, last_seen_at, last_login_at
7688
- FROM ${TABLES.STICKER_WEB_GOOGLE_USER}
7689
- WHERE google_sub LIKE ? OR email LIKE ? OR name LIKE ? OR owner_jid LIKE ? OR owner_phone LIKE ?
7690
- ORDER BY COALESCE(last_seen_at, last_login_at, created_at) DESC
7691
- LIMIT ${limit}`,
7692
- [like, like, like, like, like],
7693
- ).catch(() => []),
7694
- executeQuery(
7695
- `SELECT session_token, google_sub, email, name, owner_jid, owner_phone, last_seen_at, expires_at
7696
- FROM ${TABLES.STICKER_WEB_GOOGLE_SESSION}
7697
- WHERE revoked_at IS NULL
7698
- AND expires_at > UTC_TIMESTAMP()
7699
- AND (session_token LIKE ? OR google_sub LIKE ? OR email LIKE ? OR name LIKE ? OR owner_jid LIKE ? OR owner_phone LIKE ?)
7700
- ORDER BY COALESCE(last_seen_at, created_at) DESC
7701
- LIMIT ${limit}`,
7702
- [like, like, like, like, like, like],
7703
- ).catch(() => []),
7704
- executeQuery(
7705
- `SELECT
7706
- gm.id,
7707
- COALESCE(NULLIF(gm.subject, ''), ch.name, gm.id) AS subject,
7708
- gm.owner_jid,
7709
- gm.updated_at
7710
- FROM ${TABLES.GROUPS_METADATA} gm
7711
- LEFT JOIN ${TABLES.CHATS} ch ON ch.id = gm.id
7712
- WHERE gm.id LIKE ? OR gm.subject LIKE ? OR ch.name LIKE ? OR gm.owner_jid LIKE ?
7713
- ORDER BY gm.updated_at DESC
7714
- LIMIT ${limit}`,
7715
- [like, like, like, like],
7716
- ).catch(() => []),
7717
- listAdminPacks({
7718
- searchParams: new URLSearchParams([
7719
- ['q', q],
7720
- ['limit', String(limit)],
7721
- ]),
7722
- }).catch(() => []),
7723
- ]);
7724
-
7725
- const users = (Array.isArray(usersRows) ? usersRows : []).map((row) => ({
7726
- google_sub: normalizeGoogleSubject(row?.google_sub),
7727
- email: normalizeEmail(row?.email) || null,
7728
- name: sanitizeText(row?.name || '', 120, { allowEmpty: true }) || null,
7729
- owner_jid: normalizeJid(row?.owner_jid) || null,
7730
- owner_phone: toWhatsAppPhoneDigits(row?.owner_phone || row?.owner_jid) || null,
7731
- last_seen_at: toIsoOrNull(row?.last_seen_at),
7732
- last_login_at: toIsoOrNull(row?.last_login_at),
7733
- }));
7734
-
7735
- const sessions = (Array.isArray(sessionsRows) ? sessionsRows : []).map((row) => ({
7736
- session_token: String(row?.session_token || '').trim() || null,
7737
- google_sub: normalizeGoogleSubject(row?.google_sub),
7738
- email: normalizeEmail(row?.email) || null,
7739
- name: sanitizeText(row?.name || '', 120, { allowEmpty: true }) || null,
7740
- owner_jid: normalizeJid(row?.owner_jid) || null,
7741
- owner_phone: toWhatsAppPhoneDigits(row?.owner_phone || row?.owner_jid) || null,
7742
- last_seen_at: toIsoOrNull(row?.last_seen_at),
7743
- expires_at: toIsoOrNull(row?.expires_at),
7744
- }));
7745
-
7746
- const groups = (Array.isArray(groupsRows) ? groupsRows : []).map((row) => ({
7747
- id: String(row?.id || '').trim(),
7748
- subject: sanitizeText(row?.subject || row?.id || '', 255, { allowEmpty: true }) || String(row?.id || '').trim(),
7749
- owner_jid: normalizeJid(row?.owner_jid) || null,
7750
- updated_at: toIsoOrNull(row?.updated_at),
7751
- }));
7752
-
7753
- sendJson(req, res, 200, {
7754
- data: {
7755
- q,
7756
- totals: {
7757
- users: users.length,
7758
- sessions: sessions.length,
7759
- groups: groups.length,
7760
- packs: Array.isArray(packs) ? packs.length : 0,
7761
- },
7762
- results: {
7763
- users,
7764
- sessions,
7765
- groups,
7766
- packs: Array.isArray(packs) ? packs : [],
7767
- },
7768
- },
7769
- });
7770
- };
7771
-
7772
- const toCsvValue = (value) => {
7773
- const normalized = value === null || value === undefined ? '' : String(value);
7774
- if (/[",\n;]/.test(normalized)) {
7775
- return `"${normalized.replaceAll('"', '""')}"`;
7776
- }
7777
- return normalized;
7778
- };
7779
-
7780
- const buildCsv = (rows = [], headers = []) => {
7781
- const safeRows = Array.isArray(rows) ? rows : [];
7782
- const safeHeaders = Array.isArray(headers) ? headers : [];
7783
- const lines = [];
7784
- lines.push(safeHeaders.map((header) => toCsvValue(header)).join(','));
7785
- for (const row of safeRows) {
7786
- lines.push(
7787
- safeHeaders
7788
- .map((header) => {
7789
- const value = row && typeof row === 'object' ? row[header] : '';
7790
- return toCsvValue(value);
7791
- })
7792
- .join(','),
7793
- );
7794
- }
7795
- return `${lines.join('\n')}\n`;
7796
- };
7797
-
7798
- const handleAdminExportRequest = async (req, res, url) => {
7799
- const adminSession = requireAdminPanelSession(req, res);
7800
- if (!adminSession) return;
7801
- if (!['GET', 'HEAD'].includes(req.method || '')) {
7802
- sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
7803
- return;
7804
- }
7805
-
7806
- const format = String(url?.searchParams?.get('format') || 'json')
7807
- .trim()
7808
- .toLowerCase();
7809
- const type = String(url?.searchParams?.get('type') || 'metrics')
7810
- .trim()
7811
- .toLowerCase();
7812
-
7813
- const overview = await buildAdminOverviewPayload({ adminSession });
7814
- const exportData =
7815
- type === 'events'
7816
- ? {
7817
- moderation_queue: overview?.moderation_queue || [],
7818
- audit_log: overview?.audit_log || [],
7819
- blocked_accounts: overview?.users_sessions?.blocked_accounts || [],
7820
- }
7821
- : {
7822
- dashboard_quick: overview?.dashboard_quick || null,
7823
- counters: overview?.counters || null,
7824
- system_health: overview?.system_health || null,
7825
- alerts: overview?.alerts || [],
7826
- feature_flags: overview?.feature_flags || [],
7827
- };
7828
-
7829
- await createAdminActionAuditEvent({
7830
- adminSession,
7831
- action: 'export_data',
7832
- targetType: 'admin_export',
7833
- targetId: `${type}.${format}`,
7834
- details: { type, format },
7835
- });
7836
-
7837
- if (format !== 'csv') {
7838
- sendJson(req, res, 200, {
7839
- data: {
7840
- type,
7841
- format: 'json',
7842
- exported_at: new Date().toISOString(),
7843
- payload: exportData,
7844
- },
7845
- });
7846
- return;
7847
- }
7848
-
7849
- let headers = [];
7850
- let rows = [];
7851
-
7852
- if (type === 'events') {
7853
- headers = ['section', 'id', 'event_type', 'severity', 'title', 'subtitle', 'status', 'created_at'];
7854
- rows = [
7855
- ...(Array.isArray(exportData?.moderation_queue) ? exportData.moderation_queue : []).map((item) => ({
7856
- section: 'moderation_queue',
7857
- id: item?.id || '',
7858
- event_type: item?.event_type || '',
7859
- severity: item?.severity || '',
7860
- title: item?.title || '',
7861
- subtitle: item?.subtitle || '',
7862
- status: item?.revoked_at ? 'revoked' : item?.status || '',
7863
- created_at: item?.created_at || item?.revoked_at || '',
7864
- })),
7865
- ...(Array.isArray(exportData?.audit_log) ? exportData.audit_log : []).map((item) => ({
7866
- section: 'audit_log',
7867
- id: item?.id || '',
7868
- event_type: item?.action || '',
7869
- severity: item?.status || '',
7870
- title: item?.target_type || '',
7871
- subtitle: item?.target_id || '',
7872
- status: item?.status || '',
7873
- created_at: item?.created_at || '',
7874
- })),
7875
- ...(Array.isArray(exportData?.blocked_accounts) ? exportData.blocked_accounts : []).map((item) => ({
7876
- section: 'blocked_accounts',
7877
- id: item?.id || '',
7878
- event_type: 'ban',
7879
- severity: item?.revoked_at ? 'low' : 'critical',
7880
- title: item?.email || item?.owner_jid || item?.google_sub || '',
7881
- subtitle: item?.reason || '',
7882
- status: item?.revoked_at ? 'revoked' : 'active',
7883
- created_at: item?.created_at || '',
7884
- })),
7885
- ];
7886
- } else {
7887
- headers = ['section', 'key', 'value'];
7888
- const dashboard = exportData?.dashboard_quick || {};
7889
- const counters = exportData?.counters || {};
7890
- const health = exportData?.system_health || {};
7891
- const alerts = Array.isArray(exportData?.alerts) ? exportData.alerts : [];
7892
- const flags = Array.isArray(exportData?.feature_flags) ? exportData.feature_flags : [];
7893
- 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}%)` }))];
7894
- }
7895
-
7896
- const csv = buildCsv(rows, headers);
7897
- const stamp = new Date().toISOString().slice(0, 19).replace(/[:T]/g, '-');
7898
- res.statusCode = 200;
7899
- res.setHeader('Content-Type', 'text/csv; charset=utf-8');
7900
- res.setHeader('Content-Disposition', `attachment; filename="admin-${type}-${stamp}.csv"`);
7901
- res.end(csv);
7902
- };
7903
-
7904
- const handleAdminModeratorsRequest = async (req, res) => {
7905
- const adminSession = requireOwnerAdminPanelSession(req, res);
7906
- if (!adminSession) return;
7907
-
7908
- if (req.method === 'GET' || req.method === 'HEAD') {
7909
- const moderators = await listAdminModerators({ activeOnly: false, limit: 500 });
7910
- sendJson(req, res, 200, { data: { moderators } });
7911
- return;
7912
- }
7913
-
7914
- if (req.method !== 'POST') {
7915
- sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
7916
- return;
7917
- }
7918
-
7919
- let payload = {};
7920
- try {
7921
- payload = await readJsonBody(req);
7922
- } catch (error) {
7923
- sendJson(req, res, Number(error?.statusCode || 400), { error: error?.message || 'Body inválido.' });
7924
- return;
7925
- }
7926
-
7927
- try {
7928
- const result = await upsertAdminModeratorRecord({
7929
- googleSub: payload?.google_sub,
7930
- email: payload?.email,
7931
- ownerJid: payload?.owner_jid,
7932
- password: payload?.password,
7933
- adminSession,
7934
- });
7935
- await createAdminActionAuditEvent({
7936
- adminSession,
7937
- action: result.created ? 'moderator_create' : 'moderator_update',
7938
- targetType: 'moderator',
7939
- targetId: result?.moderator?.google_sub || payload?.google_sub || '',
7940
- details: { created: Boolean(result.created) },
7941
- });
7942
- sendJson(req, res, result.created ? 201 : 200, {
7943
- data: {
7944
- created: result.created,
7945
- moderator: result.moderator,
7946
- },
7947
- });
7948
- } catch (error) {
7949
- sendJson(req, res, Number(error?.statusCode || 400), { error: error?.message || 'Falha ao salvar moderador.' });
7950
- }
7951
- };
7952
-
7953
- const handleAdminModeratorDeleteRequest = async (req, res, googleSub) => {
7954
- const adminSession = requireOwnerAdminPanelSession(req, res);
7955
- if (!adminSession) return;
7956
- if (req.method !== 'DELETE') {
7957
- sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
7958
- return;
7959
- }
7960
- try {
7961
- const moderator = await revokeAdminModeratorRecord(googleSub, adminSession);
7962
- await createAdminActionAuditEvent({
7963
- adminSession,
7964
- action: 'moderator_revoke',
7965
- targetType: 'moderator',
7966
- targetId: moderator?.google_sub || googleSub,
7967
- });
7968
- sendJson(req, res, 200, { data: { revoked: true, moderator } });
7969
- } catch (error) {
7970
- sendJson(req, res, Number(error?.statusCode || 400), { error: error?.message || 'Falha ao remover moderador.' });
7971
- }
7972
- };
7973
-
7974
- const handleAdminPacksRequest = async (req, res, url) => {
7975
- const adminSession = requireAdminPanelSession(req, res);
7976
- if (!adminSession) return;
7977
- if (!['GET', 'HEAD'].includes(req.method || '')) {
7978
- sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
7979
- return;
7980
- }
7981
- const packs = await listAdminPacks(url);
7982
- sendJson(req, res, 200, { data: packs });
7983
- };
7984
-
7985
- const handleAdminPackDetailsRequest = async (req, res, packKey) => {
7986
- const adminSession = requireAdminPanelSession(req, res);
7987
- if (!adminSession) return;
7988
- if (!['GET', 'HEAD'].includes(req.method || '')) {
7989
- sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
7990
- return;
7991
- }
7992
- const context = await findAdminPackContextByKey(packKey);
7993
- if (!context?.fullPack) {
7994
- sendJson(req, res, 404, { error: 'Pack nao encontrado.' });
7995
- return;
7996
- }
7997
- const data = await buildManagedPackResponseData(context.fullPack);
7998
- sendJson(req, res, 200, { data });
7999
- };
8000
-
8001
- const handleAdminPackDeleteRequest = async (req, res, packKey) => {
8002
- const adminSession = requireAdminPanelSession(req, res);
8003
- if (!adminSession) return;
8004
- if (req.method !== 'DELETE') {
8005
- sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
8006
- return;
8007
- }
8008
- const context = await findAdminPackContextByKey(packKey);
8009
- const normalizedPackKey = sanitizeText(packKey, 160, { allowEmpty: false }) || String(packKey || '');
8010
- if (!context?.fullPack) {
8011
- sendManagedMutationStatus(req, res, 'already_deleted', {
8012
- deleted: false,
8013
- pack_key: normalizedPackKey,
8014
- admin: true,
8015
- });
8016
- return;
8017
- }
8018
- const result = await deleteManagedPackWithCleanup({
8019
- ownerJid: context.ownerJid,
8020
- identifier: context.packKey,
8021
- fallbackPack: context.fullPack,
8022
- });
8023
- await createAdminActionAuditEvent({
8024
- adminSession,
8025
- action: 'pack_delete',
8026
- targetType: 'pack',
8027
- targetId: result?.deletedPack?.pack_key || context.packKey || packKey,
8028
- details: {
8029
- removed_sticker_count: Number(result?.removedCount || 0),
8030
- missing: Boolean(result?.missing),
8031
- },
8032
- });
8033
- sendManagedMutationStatus(req, res, 'deleted', {
8034
- admin: true,
8035
- deleted: !result?.missing,
8036
- pack_key: result?.deletedPack?.pack_key || context.packKey,
8037
- id: result?.deletedPack?.id || context.fullPack?.id || null,
8038
- deleted_at: toIsoOrNull(result?.deletedPack?.deleted_at || new Date()),
8039
- removed_sticker_count: Number(result?.removedCount || 0),
8040
- });
8041
- };
8042
-
8043
- const handleAdminPackStickerDeleteRequest = async (req, res, packKey, stickerId) => {
8044
- const adminSession = requireAdminPanelSession(req, res);
8045
- if (!adminSession) return;
8046
- if (req.method !== 'DELETE') {
8047
- sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
8048
- return;
8049
- }
8050
- const context = await findAdminPackContextByKey(packKey);
8051
- const normalizedStickerId = sanitizeText(stickerId, 36, { allowEmpty: false });
8052
- if (!context?.fullPack) {
8053
- sendManagedMutationStatus(req, res, 'already_deleted', {
8054
- admin: true,
8055
- pack_key: sanitizeText(packKey, 160, { allowEmpty: true }) || String(packKey || ''),
8056
- sticker_id: normalizedStickerId || null,
8057
- });
8058
- return;
8059
- }
8060
- try {
8061
- const result = await stickerPackService.removeStickerFromPack({
8062
- ownerJid: context.ownerJid,
8063
- identifier: context.packKey,
8064
- selector: normalizedStickerId,
8065
- });
8066
- invalidateStickerCatalogDerivedCaches();
8067
- if (normalizedStickerId) {
8068
- await cleanupOrphanStickerAssets([normalizedStickerId], { reason: 'admin_remove_sticker' });
8069
- }
8070
- await createAdminActionAuditEvent({
8071
- adminSession,
8072
- action: 'pack_sticker_delete',
8073
- targetType: 'sticker',
8074
- targetId: normalizedStickerId || stickerId,
8075
- details: {
8076
- pack_key: context.packKey,
8077
- },
8078
- });
8079
- await sendManagedPackMutationStatus(req, res, 'updated', result?.pack || context.fullPack, {
8080
- admin: true,
8081
- pack_key: context.packKey,
8082
- removed_sticker_id: normalizedStickerId || null,
8083
- });
8084
- } catch (error) {
8085
- const mapped = mapStickerPackWebManageError(error);
8086
- sendJson(req, res, mapped.statusCode, { error: mapped.message, code: mapped.code });
8087
- }
8088
- };
8089
-
8090
- const handleAdminGlobalStickerDeleteRequest = async (req, res, stickerId) => {
8091
- const adminSession = requireAdminPanelSession(req, res);
8092
- if (!adminSession) return;
8093
- if (req.method !== 'DELETE') {
8094
- sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
8095
- return;
8096
- }
8097
- const normalizedStickerId = sanitizeText(stickerId, 36, { allowEmpty: false });
8098
- if (!normalizedStickerId) {
8099
- sendJson(req, res, 400, { error: 'sticker_id invalido.' });
8100
- return;
8101
- }
8102
-
8103
- const refs = await executeQuery(
8104
- `SELECT p.pack_key, p.owner_jid
8105
- FROM ${TABLES.STICKER_PACK_ITEM} i
8106
- INNER JOIN ${TABLES.STICKER_PACK} p ON p.id = i.pack_id
8107
- WHERE i.sticker_id = ?
8108
- AND p.deleted_at IS NULL`,
8109
- [normalizedStickerId],
8110
- );
8111
-
8112
- let removedFromPacks = 0;
8113
- let removeErrors = 0;
8114
- for (const ref of Array.isArray(refs) ? refs : []) {
8115
- try {
8116
- const ownerJid = normalizeJid(ref.owner_jid) || '';
8117
- const packKey = sanitizeText(ref.pack_key, 160, { allowEmpty: false });
8118
- if (!ownerJid || !packKey) continue;
8119
- await stickerPackService.removeStickerFromPack({
8120
- ownerJid,
8121
- identifier: packKey,
8122
- selector: normalizedStickerId,
8123
- });
8124
- removedFromPacks += 1;
8125
- } catch {
8126
- removeErrors += 1;
8127
- }
8128
- }
8129
-
8130
- const cleanup = await cleanupOrphanStickerAssets([normalizedStickerId], { reason: 'admin_delete_sticker_global' });
8131
- invalidateStickerCatalogDerivedCaches();
8132
- await createAdminActionAuditEvent({
8133
- adminSession,
8134
- action: 'global_sticker_delete',
8135
- targetType: 'sticker',
8136
- targetId: normalizedStickerId,
8137
- details: {
8138
- removed_from_packs: removedFromPacks,
8139
- remove_errors: removeErrors,
8140
- cleanup_deleted: Number(cleanup?.deleted || 0),
8141
- },
8142
- });
8143
- sendJson(req, res, 200, {
8144
- data: {
8145
- success: true,
8146
- sticker_id: normalizedStickerId,
8147
- removed_from_packs: removedFromPacks,
8148
- remove_errors: removeErrors,
8149
- cleanup,
8150
- deleted: Number(cleanup?.deleted || 0) > 0,
8151
- },
8152
- });
8153
- };
8154
-
8155
- const handleAdminBansRequest = async (req, res) => {
8156
- const adminSession = requireAdminPanelSession(req, res);
8157
- if (!adminSession) return;
8158
-
8159
- if (req.method === 'GET' || req.method === 'HEAD') {
8160
- const bans = await listAdminBans({ activeOnly: false, limit: 200 });
8161
- sendJson(req, res, 200, { data: bans });
8162
- return;
8163
- }
8164
-
8165
- if (req.method !== 'POST') {
8166
- sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
8167
- return;
8168
- }
8169
-
8170
- let payload = {};
8171
- try {
8172
- payload = await readJsonBody(req);
8173
- } catch (error) {
8174
- sendJson(req, res, Number(error?.statusCode || 400), { error: error?.message || 'Body inválido.' });
8175
- return;
8176
- }
8177
-
8178
- try {
8179
- const result = await createAdminBanRecord({
8180
- googleSub: payload?.google_sub,
8181
- email: payload?.email,
8182
- ownerJid: payload?.owner_jid,
8183
- reason: payload?.reason,
8184
- adminSession,
8185
- });
8186
- await createAdminActionAuditEvent({
8187
- adminSession,
8188
- action: result.created ? 'ban_create' : 'ban_existing',
8189
- targetType: 'ban',
8190
- targetId: result?.ban?.id || '',
8191
- details: {
8192
- google_sub: result?.ban?.google_sub || payload?.google_sub || null,
8193
- email: result?.ban?.email || payload?.email || null,
8194
- owner_jid: result?.ban?.owner_jid || payload?.owner_jid || null,
8195
- },
8196
- });
8197
- sendJson(req, res, result.created ? 201 : 200, {
8198
- data: {
8199
- created: result.created,
8200
- ban: result.ban,
8201
- },
8202
- });
8203
- } catch (error) {
8204
- sendJson(req, res, Number(error?.statusCode || 400), { error: error?.message || 'Falha ao banir usuário.' });
8205
- }
8206
- };
8207
-
8208
- const handleAdminBanRevokeRequest = async (req, res, banId) => {
8209
- const adminSession = requireAdminPanelSession(req, res);
8210
- if (!adminSession) return;
8211
- if (req.method !== 'DELETE') {
8212
- sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
8213
- return;
8214
- }
8215
- try {
8216
- const ban = await revokeAdminBanRecord(banId);
8217
- await createAdminActionAuditEvent({
8218
- adminSession,
8219
- action: 'ban_revoke',
8220
- targetType: 'ban',
8221
- targetId: ban?.id || banId,
8222
- });
8223
- sendJson(req, res, 200, { data: { revoked: true, ban } });
8224
- } catch (error) {
8225
- sendJson(req, res, Number(error?.statusCode || 400), { error: error?.message || 'Falha ao revogar ban.' });
8226
- }
8227
- };
6203
+ const { handleAdminPanelSessionRequest, handleAdminOverviewRequest, handleAdminUsersRequest, handleAdminForceLogoutRequest, handleAdminFeatureFlagsRequest, handleAdminOpsActionRequest, handleAdminSearchRequest, handleAdminExportRequest, handleAdminModeratorsRequest, handleAdminModeratorDeleteRequest, handleAdminPacksRequest, handleAdminPackDetailsRequest, handleAdminPackDeleteRequest, handleAdminPackStickerDeleteRequest, handleAdminGlobalStickerDeleteRequest, handleAdminBansRequest, handleAdminBanRevokeRequest } = createStickerCatalogAdminHandlers({
6204
+ executeQuery,
6205
+ tables: TABLES,
6206
+ logger,
6207
+ sendJson,
6208
+ readJsonBody,
6209
+ parseCookies,
6210
+ getCookieValuesFromRequest,
6211
+ appendSetCookie,
6212
+ buildCookieString,
6213
+ sanitizeText,
6214
+ normalizeGoogleSubject,
6215
+ normalizeEmail,
6216
+ normalizeJid,
6217
+ toIsoOrNull,
6218
+ toWhatsAppPhoneDigits,
6219
+ mapGoogleSessionResponseData,
6220
+ resolveGoogleWebSessionFromRequest,
6221
+ revokeGoogleWebSessionsByIdentity,
6222
+ getMarketplaceGlobalStatsCached,
6223
+ getSystemSummaryCached,
6224
+ getFeatureFlagsSnapshot,
6225
+ refreshFeatureFlags,
6226
+ listAdminBans,
6227
+ createAdminBanRecord,
6228
+ revokeAdminBanRecord,
6229
+ normalizeVisitPath,
6230
+ stickerWebPath: STICKER_WEB_PATH,
6231
+ findStickerPackByPackKey,
6232
+ stickerPackService,
6233
+ buildManagedPackResponseData,
6234
+ sendManagedMutationStatus,
6235
+ sendManagedPackMutationStatus,
6236
+ deleteManagedPackWithCleanup,
6237
+ mapStickerPackWebManageError,
6238
+ cleanupOrphanStickerAssets,
6239
+ invalidateStickerCatalogDerivedCaches,
6240
+ });
8228
6241
 
8229
6242
  const catalogApiRouter = createCatalogApiRouter({
8230
6243
  apiBasePath: STICKER_API_BASE_PATH,