@kaikybrofc/omnizap-system 2.3.1 → 2.3.3

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 (49) hide show
  1. package/README.md +82 -483
  2. package/app/controllers/messageController.js +473 -255
  3. package/app/modules/analyticsModule/messageAnalysisEventRepository.js +83 -0
  4. package/app/modules/stickerModule/stickerCommand.js +7 -2
  5. package/app/modules/stickerModule/stickerTextCommand.js +7 -2
  6. package/app/modules/stickerPackModule/stickerDomainEventConsumerRuntime.js +1 -3
  7. package/app/modules/stickerPackModule/stickerPackCommandHandlers.js +224 -53
  8. package/app/observability/metrics.js +6 -3
  9. package/app/services/googleWebLinkService.js +77 -0
  10. package/app/services/lidMapService.js +83 -4
  11. package/database/index.js +2 -0
  12. package/database/migrations/20260301_0028_message_analysis_event.sql +32 -0
  13. package/database/migrations/20260301_0029_admin_action_audit.sql +16 -0
  14. package/package.json +1 -1
  15. package/public/index.html +12 -8
  16. package/public/js/apps/createPackApp.js +4 -4
  17. package/public/js/apps/homeApp.js +78 -34
  18. package/public/js/apps/loginApp.js +245 -35
  19. package/public/js/apps/stickersAdminApp.js +4 -10
  20. package/public/js/apps/stickersApp.js +1 -1
  21. package/public/js/apps/userApp.js +956 -55
  22. package/public/js/apps/userProfileApp.js +244 -0
  23. package/public/login/index.html +437 -101
  24. package/public/termos-de-uso/index.html +1 -1
  25. package/public/user/index.html +2 -181
  26. package/public/user/systemadm/index.html +774 -0
  27. package/server/controllers/stickerCatalog/nonCatalogHandlers.js +183 -0
  28. package/server/controllers/stickerCatalogController.js +1289 -368
  29. package/server/controllers/systemAdminController.js +141 -0
  30. package/server/controllers/userController.js +87 -0
  31. package/server/http/httpServer.js +72 -32
  32. package/server/middleware/cachePolicy.js +24 -0
  33. package/server/middleware/cachePolicyHelpers.js +1 -0
  34. package/server/middleware/rateLimit.js +89 -0
  35. package/server/middleware/requestLogger.js +16 -0
  36. package/server/middleware/requireAdminAuth.js +42 -0
  37. package/server/middleware/securityHeaders.js +6 -0
  38. package/server/routes/admin/systemAdminRouter.js +56 -0
  39. package/server/routes/health/healthRouter.js +41 -0
  40. package/server/routes/indexRouter.js +197 -0
  41. package/server/routes/metrics/metricsRouter.js +13 -0
  42. package/server/routes/stickerCatalog/catalogHandlers/catalogAdminHttp.js +44 -0
  43. package/server/routes/stickerCatalog/stickerApiRouter.js +84 -0
  44. package/server/routes/stickerCatalog/stickerDataRouter.js +140 -0
  45. package/server/routes/stickerCatalog/stickerSiteRouter.js +43 -0
  46. package/server/routes/user/userRouter.js +56 -0
  47. package/server/utils/safePath.js +26 -0
  48. package/server/routes/metricsRoute.js +0 -7
  49. package/server/routes/stickerCatalogRoute.js +0 -20
@@ -21,6 +21,7 @@ import { createStickerPackInteractionEvent, listStickerPackInteractionStatsByPac
21
21
  import { buildCreatorRanking, buildIntentCollections, buildPersonalizedRecommendations, buildViewerTagAffinity, computePackSignals } from '../../app/modules/stickerPackModule/stickerPackMarketplaceService.js';
22
22
  import { listStickerPackScoreSnapshotsByPackIds } from '../../app/modules/stickerPackModule/stickerPackScoreSnapshotRepository.js';
23
23
  import { createCatalogApiRouter } from '../routes/stickerCatalog/catalogRouter.js';
24
+ import { createStickerCatalogNonCatalogHandlers } from './stickerCatalog/nonCatalogHandlers.js';
24
25
  import { createGoogleWebAuthService } from '../auth/googleWebAuth/googleWebAuthService.js';
25
26
  import { buildAdminMenu, buildAiMenu, buildAnimeMenu, buildMediaMenu, buildMenuCaption, buildQuoteMenu, buildStatsMenu, buildStickerMenu } from '../../app/modules/menuModule/common.js';
26
27
  import { getMarketplaceDriftSnapshot } from '../../app/modules/stickerPackModule/stickerMarketplaceDriftService.js';
@@ -29,7 +30,7 @@ import { convertToWebp } from '../../app/modules/stickerModule/convertToWebp.js'
29
30
  import { sanitizeText } from '../../app/modules/stickerPackModule/stickerPackUtils.js';
30
31
  import stickerPackService from '../../app/modules/stickerPackModule/stickerPackServiceRuntime.js';
31
32
  import { STICKER_PACK_ERROR_CODES, StickerPackError } from '../../app/modules/stickerPackModule/stickerPackErrors.js';
32
- import { isFeatureEnabled } from '../../app/services/featureFlagService.js';
33
+ import { getFeatureFlagsSnapshot, isFeatureEnabled, refreshFeatureFlags } from '../../app/services/featureFlagService.js';
33
34
 
34
35
  const parseEnvBool = (value, fallback) => {
35
36
  if (value === undefined || value === null || value === '') return fallback;
@@ -97,18 +98,13 @@ const STICKER_WEB_PATH = normalizeBasePath(process.env.STICKER_WEB_PATH, '/stick
97
98
  const STICKER_API_BASE_PATH = normalizeBasePath(process.env.STICKER_API_BASE_PATH, '/api/sticker-packs');
98
99
  const STICKER_ORPHAN_API_PATH = `${STICKER_API_BASE_PATH}/orphan-stickers`;
99
100
  const STICKER_CREATE_WEB_PATH = `${STICKER_WEB_PATH}/create`;
100
- const STICKER_ADMIN_WEB_PATH = `${STICKER_WEB_PATH}/admin`;
101
101
  const STICKER_LOGIN_WEB_PATH = normalizeBasePath(process.env.STICKER_LOGIN_WEB_PATH, '/login');
102
- const USER_PROFILE_WEB_PATH = normalizeBasePath(process.env.USER_PROFILE_WEB_PATH, '/user');
103
- const STICKER_ADMIN_REDIRECT_TO_USER = parseEnvBool(process.env.STICKER_ADMIN_REDIRECT_TO_USER, true);
104
102
  const STICKER_DATA_PUBLIC_PATH = normalizeBasePath(process.env.STICKER_DATA_PUBLIC_PATH, '/data');
105
103
  const STICKER_DATA_PUBLIC_DIR = path.resolve(process.env.STICKER_DATA_PUBLIC_DIR || path.join(process.cwd(), 'data'));
106
104
  const STICKER_WEB_ASSET_VERSION = sanitizeText(process.env.STICKER_WEB_ASSET_VERSION || '', 64, { allowEmpty: true }) || '';
107
105
  const CATALOG_PUBLIC_DIR = path.resolve(process.cwd(), 'public');
108
106
  const CATALOG_TEMPLATE_PATH = path.join(CATALOG_PUBLIC_DIR, 'stickers', 'index.html');
109
107
  const CREATE_PACK_TEMPLATE_PATH = path.join(CATALOG_PUBLIC_DIR, 'stickers', 'create', 'index.html');
110
- const ADMIN_PANEL_TEMPLATE_PATH = path.join(CATALOG_PUBLIC_DIR, 'stickers', 'admin', 'index.html');
111
- const USER_DASHBOARD_TEMPLATE_PATH = path.join(CATALOG_PUBLIC_DIR, 'user', 'index.html');
112
108
  const CATALOG_STYLES_FILE_PATH = path.join(CATALOG_PUBLIC_DIR, 'css', 'styles.css');
113
109
  const CATALOG_SCRIPT_FILE_PATH = path.join(CATALOG_PUBLIC_DIR, 'js', 'catalog.js');
114
110
  const DEFAULT_LIST_LIMIT = clampInt(process.env.STICKER_WEB_LIST_LIMIT, 24, 1, 60);
@@ -1007,8 +1003,11 @@ const assertGoogleIdentityNotBanned = async ({ sub = '', email = '', ownerJid =
1007
1003
 
1008
1004
  const googleWebSessionDbTouchIntervalMs = Math.max(30_000, Number(process.env.STICKER_WEB_GOOGLE_SESSION_DB_TOUCH_INTERVAL_MS) || 60_000);
1009
1005
  const googleWebSessionDbPruneIntervalMs = Math.max(5 * 60 * 1000, Number(process.env.STICKER_WEB_GOOGLE_SESSION_DB_PRUNE_INTERVAL_MS) || 60 * 60 * 1000);
1010
- const googleWebSessionCookiePath = normalizeBasePath(process.env.STICKER_WEB_GOOGLE_SESSION_COOKIE_PATH, '/');
1011
- const googleWebLegacyCookiePaths = [STICKER_API_BASE_PATH, `${STICKER_API_BASE_PATH}/auth`, STICKER_WEB_PATH, STICKER_LOGIN_WEB_PATH];
1006
+ const configuredGoogleWebSessionCookiePath = normalizeBasePath(process.env.STICKER_WEB_GOOGLE_SESSION_COOKIE_PATH, '/');
1007
+ 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
+ );
1012
1011
 
1013
1012
  const googleWebAuth = createGoogleWebAuthService({
1014
1013
  executeQuery,
@@ -1598,6 +1597,47 @@ const sumMetricValues = (series, name) => {
1598
1597
  return list.reduce((sum, entry) => sum + (Number.isFinite(entry.value) ? entry.value : 0), 0);
1599
1598
  };
1600
1599
 
1600
+ const sumMetricValuesByLabel = (series, name, matchLabels = {}) => {
1601
+ const list = series.get(name) || [];
1602
+ return list.reduce((sum, entry) => {
1603
+ if (!Number.isFinite(entry.value)) return sum;
1604
+ for (const [labelKey, expectedValue] of Object.entries(matchLabels || {})) {
1605
+ if (String(entry?.labels?.[labelKey] || '') !== String(expectedValue)) return sum;
1606
+ }
1607
+ return sum + entry.value;
1608
+ }, 0);
1609
+ };
1610
+
1611
+ const estimateHistogramQuantileMs = (series, metricBaseName, quantile = 0.95) => {
1612
+ const bucketSeries = series.get(`${metricBaseName}_bucket`) || [];
1613
+ if (!bucketSeries.length) return null;
1614
+
1615
+ const cumulativeByLe = new Map();
1616
+ for (const entry of bucketSeries) {
1617
+ const leRaw = String(entry?.labels?.le || '').trim();
1618
+ if (!leRaw) continue;
1619
+ const le = leRaw === '+Inf' ? Number.POSITIVE_INFINITY : Number(leRaw);
1620
+ if (!Number.isFinite(le) && le !== Number.POSITIVE_INFINITY) continue;
1621
+ cumulativeByLe.set(le, (cumulativeByLe.get(le) || 0) + Number(entry.value || 0));
1622
+ }
1623
+
1624
+ const sorted = Array.from(cumulativeByLe.entries()).sort((left, right) => left[0] - right[0]);
1625
+ if (!sorted.length) return null;
1626
+
1627
+ const total = Number(sorted[sorted.length - 1]?.[1] || 0);
1628
+ if (!Number.isFinite(total) || total <= 0) return null;
1629
+ const target = total * Math.max(0, Math.min(1, Number(quantile) || 0.95));
1630
+
1631
+ for (const [upperBound, cumulative] of sorted) {
1632
+ if (cumulative >= target) {
1633
+ if (!Number.isFinite(upperBound)) return null;
1634
+ return Number(upperBound.toFixed(2));
1635
+ }
1636
+ }
1637
+
1638
+ return null;
1639
+ };
1640
+
1601
1641
  const fetchPrometheusSummary = async () => {
1602
1642
  if (typeof globalThis.fetch !== 'function') {
1603
1643
  throw new Error('fetch indisponivel');
@@ -1622,6 +1662,8 @@ const fetchPrometheusSummary = async () => {
1622
1662
  const lagP99 = pickMetricValue(series, 'omnizap_nodejs_eventloop_lag_p99_seconds');
1623
1663
  const dbTotal = sumMetricValues(series, 'omnizap_db_query_total');
1624
1664
  const dbSlow = sumMetricValues(series, 'omnizap_db_slow_queries_total');
1665
+ const http5xx = sumMetricValuesByLabel(series, 'omnizap_http_requests_total', { status_class: '5xx' });
1666
+ const httpLatencyP95 = estimateHistogramQuantileMs(series, 'omnizap_http_request_duration_ms', 0.95);
1625
1667
 
1626
1668
  const queueDepthSeries = series.get('omnizap_queue_depth') || [];
1627
1669
  const queuePeak = queueDepthSeries.reduce((max, entry) => {
@@ -1634,6 +1676,8 @@ const fetchPrometheusSummary = async () => {
1634
1676
  lag_p99_ms: Number.isFinite(lagP99) ? Number((lagP99 * 1000).toFixed(2)) : null,
1635
1677
  db_total: Math.round(dbTotal || 0),
1636
1678
  db_slow: Math.round(dbSlow || 0),
1679
+ http_5xx_total: Math.round(http5xx || 0),
1680
+ http_latency_p95_ms: Number.isFinite(httpLatencyP95) ? Number(httpLatencyP95) : null,
1637
1681
  queue_peak: Math.round(queuePeak || 0),
1638
1682
  };
1639
1683
  } finally {
@@ -1686,10 +1730,31 @@ const toImageMimeType = (filePath) => {
1686
1730
  };
1687
1731
 
1688
1732
  const normalizePhoneDigits = (value) => String(value || '').replace(/\D+/g, '');
1733
+ const resolveActiveSocketBotJid = (activeSocket) => {
1734
+ if (!activeSocket) return '';
1735
+ const candidates = [activeSocket?.user?.id, activeSocket?.authState?.creds?.me?.id, activeSocket?.authState?.creds?.me?.lid];
1736
+ for (const candidate of candidates) {
1737
+ const resolved = resolveBotJid(candidate) || '';
1738
+ if (resolved) return resolved;
1739
+ }
1740
+ return '';
1741
+ };
1742
+ const resolveSocketReadyState = (activeSocket) => {
1743
+ const raw = activeSocket?.ws?.readyState;
1744
+ if (typeof raw === 'number' && Number.isFinite(raw)) return raw;
1745
+ const normalized = String(raw || '')
1746
+ .trim()
1747
+ .toLowerCase();
1748
+ if (normalized === 'open') return 1;
1749
+ if (normalized === 'connecting') return 0;
1750
+ if (normalized === 'closing') return 2;
1751
+ if (normalized === 'closed') return 3;
1752
+ return null;
1753
+ };
1689
1754
 
1690
1755
  const resolveCatalogBotPhone = () => {
1691
1756
  const activeSocket = getActiveSocket();
1692
- const botJid = resolveBotJid(activeSocket?.user?.id);
1757
+ const botJid = resolveActiveSocketBotJid(activeSocket);
1693
1758
  const jidUser = getJidUser(botJid || '');
1694
1759
  const fromSocket = normalizePhoneDigits(jidUser);
1695
1760
  if (fromSocket) return fromSocket;
@@ -1741,7 +1806,7 @@ const _resolveWebCreateOwnerJid = async (explicitOwner = '') => {
1741
1806
  if (explicit) return explicit;
1742
1807
 
1743
1808
  const activeSocket = getActiveSocket();
1744
- const botJid = resolveBotJid(activeSocket?.user?.id);
1809
+ const botJid = resolveActiveSocketBotJid(activeSocket);
1745
1810
  const fromSocket = toOwnerJid(botJid);
1746
1811
  if (fromSocket) return fromSocket;
1747
1812
 
@@ -2024,6 +2089,8 @@ const listDataImageFiles = async () => {
2024
2089
  const PACK_TAG_MARKER_REGEX = /\[pack-tags:([^\]]+)\]/i;
2025
2090
  const AUTO_PACK_MARKER_REGEX = /\[(?:auto-theme|auto-tag):[^\]]+\]/gi;
2026
2091
  const AUTO_PACK_MARKER_TEST_REGEX = /\[(?:auto-theme|auto-tag):[^\]]+\]/i;
2092
+ const AUTO_PACK_COLLECTOR_MARKER = '[auto-pack:collector]';
2093
+ const AUTO_PACK_COLLECTOR_LEGACY_TEXT = 'coleção automática de figurinhas criadas pelo usuário.';
2027
2094
  const AUTO_PACK_DESCRIPTION_PREFIX_REGEX = /^curadoria automática por tema\.\s*tema:\s*[^.]+\.?\s*(?:score\s*=\s*-?\d+(?:\.\d+)?\.?\s*)?/i;
2028
2095
  const AUTO_PACK_SCORE_FRAGMENT_REGEX = /\bscore\s*=\s*-?\d+(?:\.\d+)?\.?/gi;
2029
2096
  const normalizePackTag = (value) =>
@@ -2082,6 +2149,31 @@ const parsePackDescriptionMetadata = (description) => {
2082
2149
  };
2083
2150
  };
2084
2151
 
2152
+ const isCollectorAutoPack = (pack) => {
2153
+ if (!pack || typeof pack !== 'object') return false;
2154
+ const description = String(pack.description || '').toLowerCase();
2155
+ return description.includes(AUTO_PACK_COLLECTOR_MARKER) || description.includes(AUTO_PACK_COLLECTOR_LEGACY_TEXT);
2156
+ };
2157
+
2158
+ const isThemeCurationAutoPack = (pack) => {
2159
+ if (!pack || typeof pack !== 'object') return false;
2160
+ const name = String(pack.name || '').trim();
2161
+ if (/^\[auto\]/i.test(name)) return true;
2162
+
2163
+ const description = String(pack.description || '').toLowerCase();
2164
+ if (description.includes('[auto-theme:') || description.includes('[auto-tag:')) return true;
2165
+
2166
+ return Boolean(String(pack.pack_theme_key || '').trim());
2167
+ };
2168
+
2169
+ const shouldHidePackFromMyProfileDefault = (pack, { includeAutoPacks = false } = {}) => {
2170
+ if (!pack || typeof pack !== 'object') return false;
2171
+ if (includeAutoPacks) return false;
2172
+ if (isCollectorAutoPack(pack)) return false;
2173
+ if (isThemeCurationAutoPack(pack)) return true;
2174
+ return pack.is_auto_pack === true || Number(pack.is_auto_pack || 0) === 1;
2175
+ };
2176
+
2085
2177
  const buildPackDescriptionWithTags = (description, tags = []) => {
2086
2178
  const cleanDescription = sanitizeText(description || '', PACK_CREATE_MAX_DESCRIPTION_LENGTH, { allowEmpty: true }) || '';
2087
2179
  const normalizedTags = mergeUniqueTags(tags).slice(0, 8);
@@ -2894,39 +2986,6 @@ const renderCreatePackHtml = async () => {
2894
2986
  return html;
2895
2987
  };
2896
2988
 
2897
- const renderAdminPanelHtml = async () => {
2898
- const template = await fs.readFile(ADMIN_PANEL_TEMPLATE_PATH, 'utf8');
2899
- const replacements = {
2900
- __STICKER_WEB_PATH__: escapeHtmlAttribute(STICKER_WEB_PATH),
2901
- __STICKER_ADMIN_WEB_PATH__: escapeHtmlAttribute(STICKER_ADMIN_WEB_PATH),
2902
- __STICKER_API_BASE_PATH__: escapeHtmlAttribute(STICKER_API_BASE_PATH),
2903
- __CURRENT_YEAR__: String(new Date().getFullYear()),
2904
- };
2905
-
2906
- let html = template;
2907
- for (const [token, value] of Object.entries(replacements)) {
2908
- html = html.replaceAll(token, value);
2909
- }
2910
- return html;
2911
- };
2912
-
2913
- const renderUserDashboardHtml = async () => {
2914
- const template = await fs.readFile(USER_DASHBOARD_TEMPLATE_PATH, 'utf8');
2915
- const replacements = {
2916
- __STICKER_WEB_PATH__: escapeHtmlAttribute(STICKER_WEB_PATH),
2917
- __STICKER_LOGIN_WEB_PATH__: escapeHtmlAttribute(STICKER_LOGIN_WEB_PATH),
2918
- __STICKER_API_BASE_PATH__: escapeHtmlAttribute(STICKER_API_BASE_PATH),
2919
- __USER_PROFILE_WEB_PATH__: escapeHtmlAttribute(USER_PROFILE_WEB_PATH),
2920
- __CURRENT_YEAR__: String(new Date().getFullYear()),
2921
- };
2922
-
2923
- let html = template;
2924
- for (const [token, value] of Object.entries(replacements)) {
2925
- html = html.replaceAll(token, value);
2926
- }
2927
- return html;
2928
- };
2929
-
2930
2989
  const buildSitemapXml = async () => {
2931
2990
  if (SITEMAP_CACHE.expiresAt > Date.now() && SITEMAP_CACHE.xml) {
2932
2991
  return SITEMAP_CACHE.xml;
@@ -3299,6 +3358,39 @@ const handleMarketplaceStatsRequest = async (req, res, url) => {
3299
3358
  }
3300
3359
  };
3301
3360
 
3361
+ 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;
3382
+
3383
+ 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,
3390
+ updated_at: new Date().toISOString(),
3391
+ };
3392
+ };
3393
+
3302
3394
  const handleHomeBootstrapRequest = async (req, res, url) => {
3303
3395
  const visibility = normalizeCatalogVisibility(url?.searchParams?.get('visibility'));
3304
3396
  const fetchTimeoutMs = {
@@ -3307,6 +3399,7 @@ const handleHomeBootstrapRequest = async (req, res, url) => {
3307
3399
  session: 450,
3308
3400
  stats: 700,
3309
3401
  system_summary: 700,
3402
+ home_realtime: 700,
3310
3403
  };
3311
3404
  const errors = [];
3312
3405
 
@@ -3361,6 +3454,16 @@ const handleHomeBootstrapRequest = async (req, res, url) => {
3361
3454
  });
3362
3455
  }
3363
3456
 
3457
+ let homeRealtimePayload = null;
3458
+ try {
3459
+ homeRealtimePayload = await withTimeout(buildHomeRealtimeSnapshot({ systemSummary: systemSummaryPayload?.data || null }), fetchTimeoutMs.home_realtime);
3460
+ } catch (error) {
3461
+ errors.push({
3462
+ source: 'home_realtime',
3463
+ message: error?.message || 'home_realtime_unavailable',
3464
+ });
3465
+ }
3466
+
3364
3467
  sendJson(req, res, 200, {
3365
3468
  data: {
3366
3469
  support,
@@ -3369,6 +3472,7 @@ const handleHomeBootstrapRequest = async (req, res, url) => {
3369
3472
  stats: statsPayload?.data || null,
3370
3473
  stats_filters: statsPayload?.filters || null,
3371
3474
  system_summary: systemSummaryPayload?.data || null,
3475
+ home_realtime: homeRealtimePayload,
3372
3476
  },
3373
3477
  meta: {
3374
3478
  visibility,
@@ -3485,6 +3589,10 @@ const resolveMyProfileOwnerCandidates = async (session) => {
3485
3589
  const trustedPhones = new Set();
3486
3590
  const blockedJids = new Set();
3487
3591
  const blockedPhones = new Set();
3592
+ const normalizedSub = normalizeGoogleSubject(session?.sub);
3593
+ const normalizedEmail = normalizeEmail(session?.email);
3594
+ const normalizedSessionOwnerJid = normalizeJid(session?.ownerJid || '') || '';
3595
+ const normalizedSessionOwnerPhone = toWhatsAppPhoneDigits(session?.ownerPhone || session?.ownerJid) || '';
3488
3596
 
3489
3597
  appendCandidate(session?.ownerJid);
3490
3598
  appendCandidate(toWhatsAppOwnerJid(session?.ownerPhone || session?.ownerJid));
@@ -3493,7 +3601,7 @@ const resolveMyProfileOwnerCandidates = async (session) => {
3493
3601
  }
3494
3602
 
3495
3603
  const activeSocket = getActiveSocket();
3496
- const botJid = normalizeJid(resolveBotJid(activeSocket?.user?.id || '') || '');
3604
+ const botJid = normalizeJid(resolveActiveSocketBotJid(activeSocket) || '');
3497
3605
  if (botJid) {
3498
3606
  blockedJids.add(botJid);
3499
3607
  for (const phone of buildPhoneSet(botJid)) {
@@ -3512,25 +3620,69 @@ const resolveMyProfileOwnerCandidates = async (session) => {
3512
3620
  }
3513
3621
  }
3514
3622
 
3515
- const normalizedSub = normalizeGoogleSubject(session?.sub);
3623
+ const identityClauses = [];
3624
+ const identityParams = [];
3516
3625
  if (normalizedSub) {
3626
+ identityClauses.push('google_sub = ?');
3627
+ identityParams.push(normalizedSub);
3628
+ }
3629
+ if (normalizedEmail) {
3630
+ identityClauses.push('email = ?');
3631
+ identityParams.push(normalizedEmail);
3632
+ }
3633
+ if (normalizedSessionOwnerJid) {
3634
+ identityClauses.push('owner_jid = ?');
3635
+ identityParams.push(normalizedSessionOwnerJid);
3636
+ }
3637
+ if (normalizedSessionOwnerPhone) {
3638
+ identityClauses.push('owner_phone = ?');
3639
+ identityParams.push(normalizedSessionOwnerPhone);
3640
+ }
3641
+
3642
+ if (identityClauses.length) {
3517
3643
  try {
3518
- const rows = await executeQuery(
3644
+ const userRows = await executeQuery(
3519
3645
  `SELECT owner_jid, owner_phone
3520
3646
  FROM ${TABLES.STICKER_WEB_GOOGLE_USER}
3521
- WHERE google_sub = ?
3522
- LIMIT 1`,
3523
- [normalizedSub],
3647
+ WHERE ${identityClauses.join(' OR ')}
3648
+ ORDER BY COALESCE(last_seen_at, last_login_at, updated_at, created_at) DESC
3649
+ LIMIT 10`,
3650
+ identityParams,
3524
3651
  );
3525
- const row = Array.isArray(rows) ? rows[0] : null;
3526
- appendCandidate(row?.owner_jid || '');
3527
- appendCandidate(row?.owner_phone || '');
3528
- const mappedResolved = await resolveUserId(extractUserIdInfo(row?.owner_jid || row?.owner_phone || null)).catch(() => null);
3529
- if (mappedResolved) appendCandidate(mappedResolved);
3652
+
3653
+ for (const row of Array.isArray(userRows) ? userRows : []) {
3654
+ appendCandidate(row?.owner_jid || '');
3655
+ appendCandidate(row?.owner_phone || '');
3656
+ for (const phone of buildPhoneSet(row?.owner_jid, row?.owner_phone)) {
3657
+ trustedPhones.add(phone);
3658
+ }
3659
+ const mappedResolved = await resolveUserId(extractUserIdInfo(row?.owner_jid || row?.owner_phone || null)).catch(() => null);
3660
+ if (mappedResolved) appendCandidate(mappedResolved);
3661
+ }
3662
+
3663
+ const sessionRows = await executeQuery(
3664
+ `SELECT owner_jid, owner_phone
3665
+ FROM ${TABLES.STICKER_WEB_GOOGLE_SESSION}
3666
+ WHERE revoked_at IS NULL
3667
+ AND expires_at > UTC_TIMESTAMP()
3668
+ AND (${identityClauses.join(' OR ')})
3669
+ ORDER BY COALESCE(last_seen_at, created_at) DESC
3670
+ LIMIT 20`,
3671
+ identityParams,
3672
+ ).catch(() => []);
3673
+
3674
+ for (const row of Array.isArray(sessionRows) ? sessionRows : []) {
3675
+ appendCandidate(row?.owner_jid || '');
3676
+ appendCandidate(row?.owner_phone || '');
3677
+ for (const phone of buildPhoneSet(row?.owner_jid, row?.owner_phone)) {
3678
+ trustedPhones.add(phone);
3679
+ }
3680
+ }
3530
3681
  } catch (error) {
3531
3682
  logger.warn('Falha ao resolver owners para perfil web.', {
3532
3683
  action: 'sticker_pack_my_profile_owner_candidates_failed',
3533
3684
  google_sub: normalizedSub,
3685
+ email: normalizedEmail,
3534
3686
  error: error?.message,
3535
3687
  });
3536
3688
  }
@@ -3552,13 +3704,15 @@ const resolveMyProfileOwnerCandidates = async (session) => {
3552
3704
  const chunk = lookupValues.slice(offset, offset + 200);
3553
3705
  if (!chunk.length) continue;
3554
3706
  const placeholders = chunk.map(() => '?').join(', ');
3707
+ const lookupParams = [...chunk, ...chunk];
3555
3708
  const rows = await executeQuery(
3556
3709
  `SELECT lid, jid
3557
3710
  FROM ${TABLES.LID_MAP}
3558
3711
  WHERE jid IN (${placeholders})
3712
+ OR lid IN (${placeholders})
3559
3713
  ORDER BY last_seen DESC
3560
3714
  LIMIT 500`,
3561
- chunk,
3715
+ lookupParams,
3562
3716
  ).catch(() => []);
3563
3717
 
3564
3718
  for (const row of Array.isArray(rows) ? rows : []) {
@@ -3566,6 +3720,27 @@ const resolveMyProfileOwnerCandidates = async (session) => {
3566
3720
  const resolvedLid = normalizeJid(row?.lid || '');
3567
3721
  if (resolvedLid) lidCandidates.add(resolvedLid);
3568
3722
  }
3723
+
3724
+ const packOwnerRows = await executeQuery(
3725
+ `SELECT DISTINCT p.owner_jid
3726
+ FROM ${TABLES.STICKER_PACK} p
3727
+ INNER JOIN ${TABLES.LID_MAP} lm
3728
+ ON lm.lid = p.owner_jid
3729
+ WHERE p.deleted_at IS NULL
3730
+ AND (
3731
+ lm.jid IN (${placeholders})
3732
+ OR lm.lid IN (${placeholders})
3733
+ )
3734
+ LIMIT 500`,
3735
+ lookupParams,
3736
+ ).catch(() => []);
3737
+
3738
+ for (const row of Array.isArray(packOwnerRows) ? packOwnerRows : []) {
3739
+ const packOwnerLid = normalizeJid(row?.owner_jid || '');
3740
+ if (!packOwnerLid) continue;
3741
+ appendCandidate(packOwnerLid);
3742
+ lidCandidates.add(packOwnerLid);
3743
+ }
3569
3744
  }
3570
3745
 
3571
3746
  for (const lid of lidCandidates) {
@@ -3626,7 +3801,7 @@ const handleMyProfileRequest = async (req, res, url = null) => {
3626
3801
  client_id: STICKER_WEB_GOOGLE_CLIENT_ID || null,
3627
3802
  };
3628
3803
 
3629
- if (!session?.ownerJid) {
3804
+ if (!session?.ownerJid && !session?.email && !session?.ownerPhone) {
3630
3805
  sendJson(req, res, 200, {
3631
3806
  data: {
3632
3807
  auth: { google: authGoogle },
@@ -3647,12 +3822,13 @@ const handleMyProfileRequest = async (req, res, url = null) => {
3647
3822
  }
3648
3823
 
3649
3824
  const ownerCandidates = await resolveMyProfileOwnerCandidates(session);
3825
+ const primaryOwnerJid = normalizeJid(session?.ownerJid || '') || ownerCandidates[0] || null;
3650
3826
  if (!ownerCandidates.length) {
3651
3827
  sendJson(req, res, 200, {
3652
3828
  data: {
3653
3829
  auth: { google: authGoogle },
3654
3830
  session: mapGoogleSessionResponseData(session),
3655
- owner_jid: session.ownerJid,
3831
+ owner_jid: primaryOwnerJid,
3656
3832
  owner_jids: [],
3657
3833
  packs: [],
3658
3834
  stats: {
@@ -3675,7 +3851,7 @@ const handleMyProfileRequest = async (req, res, url = null) => {
3675
3851
  for (const packList of ownerPacks) {
3676
3852
  for (const pack of Array.isArray(packList) ? packList : []) {
3677
3853
  if (!pack?.id) continue;
3678
- if (!includeAutoPacks && pack?.is_auto_pack === true) continue;
3854
+ if (shouldHidePackFromMyProfileDefault(pack, { includeAutoPacks })) continue;
3679
3855
  const existing = dedupPacks.get(pack.id);
3680
3856
  if (!existing) {
3681
3857
  dedupPacks.set(pack.id, pack);
@@ -3731,7 +3907,7 @@ const handleMyProfileRequest = async (req, res, url = null) => {
3731
3907
  data: {
3732
3908
  auth: { google: authGoogle },
3733
3909
  session: mapGoogleSessionResponseData(session),
3734
- owner_jid: session.ownerJid,
3910
+ owner_jid: primaryOwnerJid,
3735
3911
  owner_jids: ownerCandidates,
3736
3912
  packs: mappedPacks,
3737
3913
  stats,
@@ -5527,10 +5703,10 @@ const buildSystemSummarySnapshot = async () => {
5527
5703
  let prometheusError = null;
5528
5704
  let platformError = null;
5529
5705
 
5530
- const socketReadyState = Number(activeSocket?.ws?.readyState);
5531
- const botJid = resolveBotJid(activeSocket?.user?.id) || null;
5706
+ const socketReadyState = resolveSocketReadyState(activeSocket);
5707
+ const botJid = resolveActiveSocketBotJid(activeSocket) || null;
5532
5708
  const botPhone = String(resolveCatalogBotPhone() || '').replace(/\D+/g, '') || null;
5533
- const botConnected = Boolean(botJid) && socketReadyState === 1;
5709
+ const botConnected = socketReadyState === 1 || (socketReadyState === null && Boolean(botJid));
5534
5710
  const botConnectionStatus = botConnected ? 'online' : socketReadyState === 0 ? 'connecting' : 'offline';
5535
5711
 
5536
5712
  let platform = {
@@ -5592,7 +5768,7 @@ const buildSystemSummarySnapshot = async () => {
5592
5768
  connection_status: botConnectionStatus,
5593
5769
  jid: botJid,
5594
5770
  phone: botPhone,
5595
- ready_state: Number.isFinite(socketReadyState) ? socketReadyState : null,
5771
+ ready_state: socketReadyState,
5596
5772
  },
5597
5773
  platform,
5598
5774
  host: {
@@ -5610,6 +5786,8 @@ const buildSystemSummarySnapshot = async () => {
5610
5786
  lag_p99_ms: prometheus?.lag_p99_ms ?? null,
5611
5787
  db_total: prometheus?.db_total ?? null,
5612
5788
  db_slow: prometheus?.db_slow ?? null,
5789
+ http_5xx_total: prometheus?.http_5xx_total ?? null,
5790
+ http_latency_p95_ms: prometheus?.http_latency_p95_ms ?? null,
5613
5791
  queue_peak: prometheus?.queue_peak ?? null,
5614
5792
  },
5615
5793
  updated_at: new Date().toISOString(),
@@ -5647,37 +5825,6 @@ const getSystemSummaryCached = async () => {
5647
5825
  return SYSTEM_SUMMARY_CACHE.pending;
5648
5826
  };
5649
5827
 
5650
- const handleSystemSummaryRequest = async (req, res) => {
5651
- try {
5652
- const payload = await getSystemSummaryCached();
5653
- sendJson(req, res, 200, {
5654
- ...payload,
5655
- meta: {
5656
- ...(payload.meta || {}),
5657
- cache_seconds: SYSTEM_SUMMARY_CACHE_SECONDS,
5658
- },
5659
- });
5660
- } catch (error) {
5661
- logger.warn('Falha ao montar resumo do sistema.', {
5662
- action: 'system_summary_error',
5663
- error: error?.message,
5664
- });
5665
- if (SYSTEM_SUMMARY_CACHE.value) {
5666
- sendJson(req, res, 200, {
5667
- ...SYSTEM_SUMMARY_CACHE.value,
5668
- meta: {
5669
- ...(SYSTEM_SUMMARY_CACHE.value.meta || {}),
5670
- cache_seconds: SYSTEM_SUMMARY_CACHE_SECONDS,
5671
- stale: true,
5672
- error: error?.message || 'fallback_cache',
5673
- },
5674
- });
5675
- return;
5676
- }
5677
- sendJson(req, res, 503, { error: 'Resumo do sistema indisponível no momento.' });
5678
- }
5679
- };
5680
-
5681
5828
  const withTimeout = (promise, timeoutMs) =>
5682
5829
  Promise.race([
5683
5830
  promise,
@@ -5862,57 +6009,9 @@ const getReadmeSummaryCached = async () => {
5862
6009
  return README_SUMMARY_CACHE.pending;
5863
6010
  };
5864
6011
 
5865
- const handleReadmeSummaryRequest = async (req, res) => {
5866
- try {
5867
- const payload = await getReadmeSummaryCached();
5868
- sendJson(req, res, 200, payload);
5869
- } catch (error) {
5870
- logger.warn('Falha ao montar resumo markdown para README.', {
5871
- action: 'readme_summary_error',
5872
- error: error?.message,
5873
- });
5874
- if (README_SUMMARY_CACHE.value) {
5875
- sendJson(req, res, 200, {
5876
- ...README_SUMMARY_CACHE.value,
5877
- meta: {
5878
- stale: true,
5879
- error: error?.message || 'fallback_cache',
5880
- },
5881
- });
5882
- return;
5883
- }
5884
- sendJson(req, res, 503, { error: 'Resumo markdown indisponível no momento.' });
5885
- }
5886
- };
5887
-
5888
- const handleReadmeMarkdownRequest = async (req, res) => {
5889
- try {
5890
- const payload = await getReadmeSummaryCached();
5891
- const markdown = String(payload?.data?.markdown || '').trim();
5892
- res.setHeader('X-Robots-Tag', 'noindex, nofollow');
5893
- res.setHeader('Cache-Control', `public, max-age=${Math.min(README_SUMMARY_CACHE_SECONDS, 300)}`);
5894
- res.setHeader('X-Cache-Seconds', String(README_SUMMARY_CACHE_SECONDS));
5895
- sendText(req, res, 200, markdown ? `${markdown}\n` : '', 'text/markdown; charset=utf-8');
5896
- } catch (error) {
5897
- logger.warn('Falha ao renderizar markdown para README.', {
5898
- action: 'readme_markdown_error',
5899
- error: error?.message,
5900
- });
5901
- if (README_SUMMARY_CACHE.value) {
5902
- const markdown = String(README_SUMMARY_CACHE.value?.data?.markdown || '').trim();
5903
- res.setHeader('X-Robots-Tag', 'noindex, nofollow');
5904
- res.setHeader('Cache-Control', `public, max-age=${Math.min(README_SUMMARY_CACHE_SECONDS, 300)}`);
5905
- res.setHeader('X-Cache-Seconds', String(README_SUMMARY_CACHE_SECONDS));
5906
- sendText(req, res, 200, markdown ? `${markdown}\n` : '', 'text/markdown; charset=utf-8');
5907
- return;
5908
- }
5909
- sendText(req, res, 503, 'Resumo markdown indisponivel no momento.\n', 'text/plain; charset=utf-8');
5910
- }
5911
- };
5912
-
5913
6012
  const resolveBotUserCandidates = (activeSocket) => {
5914
6013
  const candidates = new Set();
5915
- const botJidFromSocket = resolveBotJid(activeSocket?.user?.id);
6014
+ const botJidFromSocket = resolveActiveSocketBotJid(activeSocket);
5916
6015
  const botUserFromSocket = getJidUser(botJidFromSocket || '');
5917
6016
  if (botUserFromSocket) candidates.add(String(botUserFromSocket).trim());
5918
6017
  const botPhoneFromCatalog = String(resolveCatalogBotPhone() || '').replace(/\D+/g, '');
@@ -6152,29 +6251,6 @@ const scheduleGlobalRankingPreload = () => {
6152
6251
  }
6153
6252
  };
6154
6253
 
6155
- const handleGlobalRankingSummaryRequest = async (req, res) => {
6156
- const activeSocket = getActiveSocket();
6157
- const botUsers = resolveBotUserCandidates(activeSocket);
6158
- try {
6159
- const rawData = await getGlobalRankingSummaryCached();
6160
- const data = sanitizeRankingPayloadByBot(rawData, botUsers);
6161
- sendJson(req, res, 200, { data, meta: { cache_seconds: GLOBAL_RANK_REFRESH_SECONDS } });
6162
- } catch (error) {
6163
- logger.warn('Falha ao montar resumo do ranking global.', {
6164
- action: 'global_ranking_summary_error',
6165
- error: error?.message,
6166
- });
6167
- if (GLOBAL_RANK_CACHE.value) {
6168
- sendJson(req, res, 200, {
6169
- data: sanitizeRankingPayloadByBot(GLOBAL_RANK_CACHE.value, botUsers),
6170
- meta: { cache_seconds: GLOBAL_RANK_REFRESH_SECONDS, stale: true, error: error?.message || 'fallback_cache' },
6171
- });
6172
- return;
6173
- }
6174
- sendJson(req, res, 503, { error: 'Ranking global indisponível no momento.' });
6175
- }
6176
- };
6177
-
6178
6254
  const buildLastSevenUtcDateKeys = () => {
6179
6255
  const now = new Date();
6180
6256
  const todayUtc = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate());
@@ -6342,74 +6418,32 @@ const getMarketplaceGlobalStatsCached = async () => {
6342
6418
  return MARKETPLACE_GLOBAL_STATS_CACHE.pending;
6343
6419
  };
6344
6420
 
6345
- const handleMarketplaceGlobalStatsRequest = async (req, res) => {
6346
- try {
6347
- const data = await getMarketplaceGlobalStatsCached();
6348
- sendJson(req, res, 200, {
6349
- ...data,
6350
- cache_seconds: MARKETPLACE_GLOBAL_STATS_CACHE_SECONDS,
6351
- });
6352
- } catch (error) {
6353
- logger.warn('Falha ao montar stats globais do marketplace.', {
6354
- action: 'marketplace_global_stats_error',
6355
- error: error?.message,
6356
- });
6357
- if (MARKETPLACE_GLOBAL_STATS_CACHE.value) {
6358
- sendJson(req, res, 200, {
6359
- ...MARKETPLACE_GLOBAL_STATS_CACHE.value,
6360
- cache_seconds: MARKETPLACE_GLOBAL_STATS_CACHE_SECONDS,
6361
- stale: true,
6362
- });
6363
- return;
6364
- }
6365
- sendJson(req, res, 503, { error: 'Stats globais do marketplace indisponíveis no momento.' });
6366
- }
6367
- };
6368
-
6369
- const handleGitHubProjectSummaryRequest = async (req, res) => {
6370
- if (!GITHUB_REPO_INFO) {
6371
- sendJson(req, res, 500, { error: 'Configuracao de repositorio GitHub invalida.' });
6372
- return;
6373
- }
6374
-
6375
- try {
6376
- const data = await fetchGitHubProjectSummary();
6377
- sendJson(req, res, 200, {
6378
- data,
6379
- meta: {
6380
- repository: GITHUB_REPO_INFO.fullName,
6381
- token_configured: Boolean(GITHUB_TOKEN),
6382
- cache_seconds: GITHUB_PROJECT_CACHE_SECONDS,
6383
- },
6384
- });
6385
- } catch (error) {
6386
- logger.warn('Falha ao consultar resumo do repositorio no GitHub.', {
6387
- action: 'github_project_summary_error',
6388
- repository: GITHUB_REPO_INFO.fullName,
6389
- error: error?.message,
6390
- status_code: error?.statusCode || null,
6391
- });
6392
- sendJson(req, res, 502, { error: 'Falha ao consultar dados do projeto no GitHub.' });
6393
- }
6394
- };
6395
-
6396
- const handleSupportInfoRequest = async (req, res) => {
6397
- const data = await buildSupportInfo();
6398
- if (!data) {
6399
- sendJson(req, res, 404, { error: 'Contato de suporte indisponível.' });
6400
- return;
6401
- }
6402
- sendJson(req, res, 200, { data });
6403
- };
6404
-
6405
- const handleBotContactInfoRequest = async (req, res) => {
6406
- const data = buildBotContactInfo();
6407
- if (!data) {
6408
- sendJson(req, res, 404, { error: 'Contato do bot indisponivel no momento.' });
6409
- return;
6410
- }
6411
- sendJson(req, res, 200, { data });
6412
- };
6421
+ const { handleSystemSummaryRequest, handleReadmeSummaryRequest, handleReadmeMarkdownRequest, handleGlobalRankingSummaryRequest, handleMarketplaceGlobalStatsRequest, handleGitHubProjectSummaryRequest, handleSupportInfoRequest, handleBotContactInfoRequest } = createStickerCatalogNonCatalogHandlers({
6422
+ sendJson,
6423
+ sendText,
6424
+ logger,
6425
+ getSystemSummaryCached,
6426
+ systemSummaryCache: SYSTEM_SUMMARY_CACHE,
6427
+ systemSummaryCacheSeconds: SYSTEM_SUMMARY_CACHE_SECONDS,
6428
+ getReadmeSummaryCached,
6429
+ readmeSummaryCache: README_SUMMARY_CACHE,
6430
+ readmeSummaryCacheSeconds: README_SUMMARY_CACHE_SECONDS,
6431
+ getGlobalRankingSummaryCached,
6432
+ globalRankRefreshSeconds: GLOBAL_RANK_REFRESH_SECONDS,
6433
+ globalRankCache: GLOBAL_RANK_CACHE,
6434
+ sanitizeRankingPayloadByBot,
6435
+ getActiveSocket,
6436
+ resolveBotUserCandidates,
6437
+ getMarketplaceGlobalStatsCached,
6438
+ marketplaceGlobalStatsCacheSeconds: MARKETPLACE_GLOBAL_STATS_CACHE_SECONDS,
6439
+ marketplaceGlobalStatsCache: MARKETPLACE_GLOBAL_STATS_CACHE,
6440
+ githubRepoInfo: GITHUB_REPO_INFO,
6441
+ githubToken: GITHUB_TOKEN,
6442
+ githubProjectCacheSeconds: GITHUB_PROJECT_CACHE_SECONDS,
6443
+ fetchGitHubProjectSummary,
6444
+ buildSupportInfo,
6445
+ buildBotContactInfo,
6446
+ });
6413
6447
 
6414
6448
  const handlePublicDataAssetRequest = async (req, res, pathname) => {
6415
6449
  const suffix = pathname.slice(STICKER_DATA_PUBLIC_PATH.length).replace(/^\/+/, '');
@@ -6685,62 +6719,419 @@ const handlePackInteractionRequest = async (req, res, packKey, interaction, url)
6685
6719
  });
6686
6720
  };
6687
6721
 
6688
- const listAdminActiveGoogleWebSessions = async ({ limit = 200 } = {}) => {
6689
- const safeLimit = Math.max(1, Math.min(500, Number(limit || 200)));
6690
- const rows = await executeQuery(
6691
- `SELECT session_token, google_sub, owner_jid, owner_phone, email, name, picture_url, created_at, last_seen_at, expires_at
6692
- FROM ${TABLES.STICKER_WEB_GOOGLE_SESSION}
6693
- WHERE revoked_at IS NULL
6694
- AND expires_at > UTC_TIMESTAMP()
6695
- ORDER BY COALESCE(last_seen_at, created_at) DESC
6696
- LIMIT ${safeLimit}`,
6697
- );
6698
- return (Array.isArray(rows) ? rows : []).map((row) => ({
6699
- session_token: String(row.session_token || '').trim(),
6700
- google_sub: normalizeGoogleSubject(row.google_sub),
6701
- owner_jid: normalizeJid(row.owner_jid) || null,
6702
- owner_phone: toWhatsAppPhoneDigits(row.owner_phone || row.owner_jid) || null,
6703
- email: normalizeEmail(row.email) || null,
6704
- name: sanitizeText(row.name || '', 120, { allowEmpty: true }) || null,
6705
- picture: String(row.picture_url || '').trim() || null,
6706
- created_at: toIsoOrNull(row.created_at),
6707
- last_seen_at: toIsoOrNull(row.last_seen_at),
6708
- expires_at: toIsoOrNull(row.expires_at),
6709
- }));
6710
- };
6711
-
6712
- const listAdminKnownGoogleUsers = async ({ limit = 200 } = {}) => {
6713
- const safeLimit = Math.max(1, Math.min(500, Number(limit || 200)));
6714
- const rows = await executeQuery(
6715
- `SELECT google_sub, owner_jid, owner_phone, email, name, picture_url, created_at, updated_at, last_login_at, last_seen_at
6716
- FROM ${TABLES.STICKER_WEB_GOOGLE_USER}
6717
- ORDER BY COALESCE(last_seen_at, last_login_at, updated_at, created_at) DESC
6718
- LIMIT ${safeLimit}`,
6719
- );
6720
- return (Array.isArray(rows) ? rows : []).map((row) => ({
6721
- google_sub: normalizeGoogleSubject(row.google_sub),
6722
- owner_jid: normalizeJid(row.owner_jid) || null,
6723
- owner_phone: toWhatsAppPhoneDigits(row.owner_phone || row.owner_jid) || null,
6724
- email: normalizeEmail(row.email) || null,
6725
- name: sanitizeText(row.name || '', 120, { allowEmpty: true }) || null,
6726
- picture: String(row.picture_url || '').trim() || null,
6727
- created_at: toIsoOrNull(row.created_at),
6728
- updated_at: toIsoOrNull(row.updated_at),
6729
- last_login_at: toIsoOrNull(row.last_login_at),
6730
- last_seen_at: toIsoOrNull(row.last_seen_at),
6731
- }));
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
+ }
6732
6731
  };
6733
6732
 
6734
- const getWebVisitSummary = async ({ rangeDays = 7, topPathsLimit = 10 } = {}) => {
6735
- const safeRangeDays = Math.max(1, Math.min(90, Number(rangeDays || 7)));
6736
- const safeTopPathsLimit = Math.max(1, Math.min(30, Number(topPathsLimit || 10)));
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;
6737
6750
 
6738
6751
  try {
6739
- const [countersRows, topPathsRows] = await Promise.all([
6740
- executeQuery(
6741
- `SELECT
6742
- COUNT(*) AS total_events,
6743
- SUM(CASE WHEN created_at >= (UTC_TIMESTAMP() - INTERVAL 1 DAY) THEN 1 ELSE 0 END) AS events_24h,
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,
6744
7135
  SUM(CASE WHEN created_at >= (UTC_TIMESTAMP() - INTERVAL ${safeRangeDays} DAY) THEN 1 ELSE 0 END) AS events_range,
6745
7136
  COUNT(DISTINCT CASE WHEN created_at >= (UTC_TIMESTAMP() - INTERVAL ${safeRangeDays} DAY) THEN visitor_key END) AS unique_visitors_range,
6746
7137
  COUNT(DISTINCT CASE WHEN created_at >= (UTC_TIMESTAMP() - INTERVAL ${safeRangeDays} DAY) THEN session_key END) AS unique_sessions_range
@@ -6842,6 +7233,64 @@ const listAdminPacks = async (url) => {
6842
7233
  }));
6843
7234
  };
6844
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
+
6845
7294
  const findAdminPackContextByKey = async (rawPackKey) => {
6846
7295
  const packKey = sanitizeText(rawPackKey, 160, { allowEmpty: false });
6847
7296
  if (!packKey) return null;
@@ -6865,8 +7314,6 @@ const handleAdminPanelSessionRequest = async (req, res) => {
6865
7314
  const eligibility = await resolveAdminPanelLoginEligibility(googleSession);
6866
7315
  sendJson(req, res, 200, {
6867
7316
  data: {
6868
- enabled: true,
6869
- admin_email: ADMIN_PANEL_EMAIL || null,
6870
7317
  google: mapGoogleSessionResponseData(googleSession),
6871
7318
  eligible_google_login: Boolean(eligibility.eligible),
6872
7319
  eligible_role: eligibility.role || null,
@@ -6878,8 +7325,15 @@ const handleAdminPanelSessionRequest = async (req, res) => {
6878
7325
 
6879
7326
  if (req.method === 'DELETE') {
6880
7327
  const token = getAdminPanelSessionTokenFromRequest(req);
7328
+ const adminSession = resolveAdminPanelSessionFromRequest(req);
6881
7329
  if (token) adminPanelSessionMap.delete(token);
6882
7330
  clearAdminPanelSessionCookie(req, res);
7331
+ await createAdminActionAuditEvent({
7332
+ adminSession,
7333
+ action: 'admin_session_logout',
7334
+ targetType: 'admin_session',
7335
+ targetId: token || 'cookie_clear',
7336
+ });
6883
7337
  sendJson(req, res, 200, { data: { cleared: true } });
6884
7338
  return;
6885
7339
  }
@@ -6933,14 +7387,19 @@ const handleAdminPanelSessionRequest = async (req, res) => {
6933
7387
  );
6934
7388
  sendJson(req, res, 200, {
6935
7389
  data: {
6936
- enabled: true,
6937
- admin_email: ADMIN_PANEL_EMAIL,
6938
7390
  google: mapGoogleSessionResponseData(googleSession),
6939
7391
  eligible_google_login: true,
6940
7392
  eligible_role: sessionRole,
6941
7393
  session: mapAdminPanelSessionResponseData(session),
6942
7394
  },
6943
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
+ });
6944
7403
  };
6945
7404
 
6946
7405
  const handleAdminOverviewRequest = async (req, res) => {
@@ -6951,41 +7410,495 @@ const handleAdminOverviewRequest = async (req, res) => {
6951
7410
  return;
6952
7411
  }
6953
7412
 
6954
- const [marketplaceStats, activeSessions, knownUsers, bans, packsCountRows, stickersCountRows, recentPacks, visitSummary] = await Promise.all([getMarketplaceGlobalStatsCached().catch(() => null), listAdminActiveGoogleWebSessions({ limit: 50 }), listAdminKnownGoogleUsers({ limit: 50 }), listAdminBans({ activeOnly: true, limit: 50 }), 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', '20']]) }), getWebVisitSummary({ rangeDays: 7, topPathsLimit: 10 }).catch(() => null)]);
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
+ }));
6955
7752
 
6956
7753
  sendJson(req, res, 200, {
6957
7754
  data: {
6958
- admin_session: mapAdminPanelSessionResponseData(adminSession),
6959
- marketplace_stats: marketplaceStats,
6960
- counters: {
6961
- total_packs_any_status: Number(packsCountRows?.[0]?.total || 0),
6962
- total_stickers_any_status: Number(stickersCountRows?.[0]?.total || 0),
6963
- active_google_sessions: Number(activeSessions.length || 0),
6964
- known_google_users: Number(knownUsers.length || 0),
6965
- active_bans: Number(bans.length || 0),
6966
- visit_events_24h: Number(visitSummary?.events_24h || 0),
6967
- visit_events_7d: Number(visitSummary?.events_range || 0),
6968
- unique_visitors_7d: Number(visitSummary?.unique_visitors_range || 0),
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 : [],
6969
7767
  },
6970
- active_sessions: activeSessions,
6971
- users: knownUsers,
6972
- bans,
6973
- recent_packs: recentPacks,
6974
- visit_metrics: visitSummary,
6975
7768
  },
6976
7769
  });
6977
7770
  };
6978
7771
 
6979
- const handleAdminUsersRequest = async (req, res, url) => {
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) => {
6980
7799
  const adminSession = requireAdminPanelSession(req, res);
6981
7800
  if (!adminSession) return;
6982
7801
  if (!['GET', 'HEAD'].includes(req.method || '')) {
6983
7802
  sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
6984
7803
  return;
6985
7804
  }
6986
- const limit = Math.max(1, Math.min(500, Number(url?.searchParams?.get('limit') || 200)));
6987
- const [activeSessions, users] = await Promise.all([listAdminActiveGoogleWebSessions({ limit }), listAdminKnownGoogleUsers({ limit })]);
6988
- sendJson(req, res, 200, { data: { active_sessions: activeSessions, users } });
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);
6989
7902
  };
6990
7903
 
6991
7904
  const handleAdminModeratorsRequest = async (req, res) => {
@@ -7019,6 +7932,13 @@ const handleAdminModeratorsRequest = async (req, res) => {
7019
7932
  password: payload?.password,
7020
7933
  adminSession,
7021
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
+ });
7022
7942
  sendJson(req, res, result.created ? 201 : 200, {
7023
7943
  data: {
7024
7944
  created: result.created,
@@ -7039,6 +7959,12 @@ const handleAdminModeratorDeleteRequest = async (req, res, googleSub) => {
7039
7959
  }
7040
7960
  try {
7041
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
+ });
7042
7968
  sendJson(req, res, 200, { data: { revoked: true, moderator } });
7043
7969
  } catch (error) {
7044
7970
  sendJson(req, res, Number(error?.statusCode || 400), { error: error?.message || 'Falha ao remover moderador.' });
@@ -7094,6 +8020,16 @@ const handleAdminPackDeleteRequest = async (req, res, packKey) => {
7094
8020
  identifier: context.packKey,
7095
8021
  fallbackPack: context.fullPack,
7096
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
+ });
7097
8033
  sendManagedMutationStatus(req, res, 'deleted', {
7098
8034
  admin: true,
7099
8035
  deleted: !result?.missing,
@@ -7131,6 +8067,15 @@ const handleAdminPackStickerDeleteRequest = async (req, res, packKey, stickerId)
7131
8067
  if (normalizedStickerId) {
7132
8068
  await cleanupOrphanStickerAssets([normalizedStickerId], { reason: 'admin_remove_sticker' });
7133
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
+ });
7134
8079
  await sendManagedPackMutationStatus(req, res, 'updated', result?.pack || context.fullPack, {
7135
8080
  admin: true,
7136
8081
  pack_key: context.packKey,
@@ -7184,6 +8129,17 @@ const handleAdminGlobalStickerDeleteRequest = async (req, res, stickerId) => {
7184
8129
 
7185
8130
  const cleanup = await cleanupOrphanStickerAssets([normalizedStickerId], { reason: 'admin_delete_sticker_global' });
7186
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
+ });
7187
8143
  sendJson(req, res, 200, {
7188
8144
  data: {
7189
8145
  success: true,
@@ -7227,6 +8183,17 @@ const handleAdminBansRequest = async (req, res) => {
7227
8183
  reason: payload?.reason,
7228
8184
  adminSession,
7229
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
+ });
7230
8197
  sendJson(req, res, result.created ? 201 : 200, {
7231
8198
  data: {
7232
8199
  created: result.created,
@@ -7247,6 +8214,12 @@ const handleAdminBanRevokeRequest = async (req, res, banId) => {
7247
8214
  }
7248
8215
  try {
7249
8216
  const ban = await revokeAdminBanRecord(banId);
8217
+ await createAdminActionAuditEvent({
8218
+ adminSession,
8219
+ action: 'ban_revoke',
8220
+ targetType: 'ban',
8221
+ targetId: ban?.id || banId,
8222
+ });
7250
8223
  sendJson(req, res, 200, { data: { revoked: true, ban } });
7251
8224
  } catch (error) {
7252
8225
  sendJson(req, res, Number(error?.statusCode || 400), { error: error?.message || 'Falha ao revogar ban.' });
@@ -7280,6 +8253,11 @@ const catalogApiRouter = createCatalogApiRouter({
7280
8253
  handleBotContactInfoRequest,
7281
8254
  handleAdminOverviewRequest,
7282
8255
  handleAdminUsersRequest,
8256
+ handleAdminForceLogoutRequest,
8257
+ handleAdminFeatureFlagsRequest,
8258
+ handleAdminOpsActionRequest,
8259
+ handleAdminSearchRequest,
8260
+ handleAdminExportRequest,
7283
8261
  handleAdminModeratorsRequest,
7284
8262
  handleAdminModeratorDeleteRequest,
7285
8263
  handleAdminPacksRequest,
@@ -7318,39 +8296,6 @@ const handleCatalogPageRequest = async (req, res, pathname) => {
7318
8296
  });
7319
8297
  });
7320
8298
 
7321
- if (normalizedPath === STICKER_ADMIN_WEB_PATH) {
7322
- if (STICKER_ADMIN_REDIRECT_TO_USER) {
7323
- const requestUrl = new URL(req.url || `${STICKER_ADMIN_WEB_PATH}/`, SITE_ORIGIN);
7324
- const userUrl = new URL(`${USER_PROFILE_WEB_PATH}/`, SITE_ORIGIN);
7325
- for (const [key, value] of requestUrl.searchParams.entries()) {
7326
- userUrl.searchParams.append(key, value);
7327
- }
7328
- res.statusCode = 302;
7329
- res.setHeader('Location', `${userUrl.pathname}${userUrl.search}`);
7330
- res.setHeader('Cache-Control', 'no-store');
7331
- res.end();
7332
- return;
7333
- }
7334
-
7335
- try {
7336
- const html = await renderAdminPanelHtml();
7337
- sendText(req, res, 200, html, 'text/html; charset=utf-8');
7338
- return;
7339
- } catch (error) {
7340
- if (error?.code === 'ENOENT') {
7341
- sendJson(req, res, 404, { error: 'Template do painel admin nao encontrado.' });
7342
- return;
7343
- }
7344
- logger.error('Falha ao renderizar pagina do painel admin.', {
7345
- action: 'sticker_catalog_admin_page_render_failed',
7346
- path: pathname,
7347
- error: error?.message,
7348
- });
7349
- sendJson(req, res, 500, { error: 'Falha interna ao renderizar painel admin.' });
7350
- return;
7351
- }
7352
- }
7353
-
7354
8299
  if (normalizedPath === STICKER_CREATE_WEB_PATH) {
7355
8300
  try {
7356
8301
  const googleSession = await resolveGoogleWebSessionFromRequest(req);
@@ -7456,7 +8401,6 @@ export const isStickerCatalogEnabled = () => STICKER_CATALOG_ENABLED;
7456
8401
  export const getStickerCatalogConfig = () => ({
7457
8402
  enabled: STICKER_CATALOG_ENABLED,
7458
8403
  webPath: STICKER_WEB_PATH,
7459
- userProfilePath: USER_PROFILE_WEB_PATH,
7460
8404
  apiBasePath: STICKER_API_BASE_PATH,
7461
8405
  orphanApiPath: STICKER_ORPHAN_API_PATH,
7462
8406
  dataPublicPath: STICKER_DATA_PUBLIC_PATH,
@@ -7480,29 +8424,6 @@ export async function maybeHandleStickerCatalogRequest(req, res, { pathname, url
7480
8424
  if (!['GET', 'HEAD', 'POST', 'PATCH', 'DELETE'].includes(req.method || '')) return false;
7481
8425
  if (maybeRedirectToCanonicalHost(req, res, url)) return true;
7482
8426
 
7483
- if (pathname === USER_PROFILE_WEB_PATH || pathname === `${USER_PROFILE_WEB_PATH}/`) {
7484
- if (!['GET', 'HEAD'].includes(req.method || '')) return false;
7485
- try {
7486
- const html = await renderUserDashboardHtml();
7487
- res.setHeader('Cache-Control', 'no-store');
7488
- res.setHeader('X-Robots-Tag', 'noindex, nofollow');
7489
- sendText(req, res, 200, html, 'text/html; charset=utf-8');
7490
- } catch (error) {
7491
- if (error?.code === 'ENOENT') {
7492
- sendJson(req, res, 404, { error: 'Template da pagina de usuario nao encontrado.' });
7493
- return true;
7494
- }
7495
-
7496
- logger.error('Falha ao renderizar pagina de usuario.', {
7497
- action: 'user_dashboard_page_render_failed',
7498
- path: pathname,
7499
- error: error?.message,
7500
- });
7501
- sendJson(req, res, 500, { error: 'Falha interna ao renderizar pagina de usuario.' });
7502
- }
7503
- return true;
7504
- }
7505
-
7506
8427
  if (pathname === '/sitemap.xml') {
7507
8428
  if (!['GET', 'HEAD'].includes(req.method || '')) return false;
7508
8429
  try {