@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.
- package/README.md +82 -483
- package/app/controllers/messageController.js +473 -255
- package/app/modules/analyticsModule/messageAnalysisEventRepository.js +83 -0
- package/app/modules/stickerModule/stickerCommand.js +7 -2
- package/app/modules/stickerModule/stickerTextCommand.js +7 -2
- package/app/modules/stickerPackModule/stickerDomainEventConsumerRuntime.js +1 -3
- package/app/modules/stickerPackModule/stickerPackCommandHandlers.js +224 -53
- package/app/observability/metrics.js +6 -3
- package/app/services/googleWebLinkService.js +77 -0
- package/app/services/lidMapService.js +83 -4
- package/database/index.js +2 -0
- package/database/migrations/20260301_0028_message_analysis_event.sql +32 -0
- package/database/migrations/20260301_0029_admin_action_audit.sql +16 -0
- package/package.json +1 -1
- package/public/index.html +12 -8
- package/public/js/apps/createPackApp.js +4 -4
- package/public/js/apps/homeApp.js +78 -34
- package/public/js/apps/loginApp.js +245 -35
- package/public/js/apps/stickersAdminApp.js +4 -10
- package/public/js/apps/stickersApp.js +1 -1
- package/public/js/apps/userApp.js +956 -55
- package/public/js/apps/userProfileApp.js +244 -0
- package/public/login/index.html +437 -101
- package/public/termos-de-uso/index.html +1 -1
- package/public/user/index.html +2 -181
- package/public/user/systemadm/index.html +774 -0
- package/server/controllers/stickerCatalog/nonCatalogHandlers.js +183 -0
- package/server/controllers/stickerCatalogController.js +1289 -368
- package/server/controllers/systemAdminController.js +141 -0
- package/server/controllers/userController.js +87 -0
- package/server/http/httpServer.js +72 -32
- package/server/middleware/cachePolicy.js +24 -0
- package/server/middleware/cachePolicyHelpers.js +1 -0
- package/server/middleware/rateLimit.js +89 -0
- package/server/middleware/requestLogger.js +16 -0
- package/server/middleware/requireAdminAuth.js +42 -0
- package/server/middleware/securityHeaders.js +6 -0
- package/server/routes/admin/systemAdminRouter.js +56 -0
- package/server/routes/health/healthRouter.js +41 -0
- package/server/routes/indexRouter.js +197 -0
- package/server/routes/metrics/metricsRouter.js +13 -0
- package/server/routes/stickerCatalog/catalogHandlers/catalogAdminHttp.js +44 -0
- package/server/routes/stickerCatalog/stickerApiRouter.js +84 -0
- package/server/routes/stickerCatalog/stickerDataRouter.js +140 -0
- package/server/routes/stickerCatalog/stickerSiteRouter.js +43 -0
- package/server/routes/user/userRouter.js +56 -0
- package/server/utils/safePath.js +26 -0
- package/server/routes/metricsRoute.js +0 -7
- 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
|
|
1011
|
-
const
|
|
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 =
|
|
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 =
|
|
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(
|
|
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
|
|
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
|
|
3644
|
+
const userRows = await executeQuery(
|
|
3519
3645
|
`SELECT owner_jid, owner_phone
|
|
3520
3646
|
FROM ${TABLES.STICKER_WEB_GOOGLE_USER}
|
|
3521
|
-
WHERE
|
|
3522
|
-
|
|
3523
|
-
|
|
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
|
-
|
|
3526
|
-
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
|
|
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
|
-
|
|
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:
|
|
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 (
|
|
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:
|
|
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 =
|
|
5531
|
-
const botJid =
|
|
5706
|
+
const socketReadyState = resolveSocketReadyState(activeSocket);
|
|
5707
|
+
const botJid = resolveActiveSocketBotJid(activeSocket) || null;
|
|
5532
5708
|
const botPhone = String(resolveCatalogBotPhone() || '').replace(/\D+/g, '') || null;
|
|
5533
|
-
const botConnected =
|
|
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:
|
|
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 =
|
|
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
|
|
6346
|
-
|
|
6347
|
-
|
|
6348
|
-
|
|
6349
|
-
|
|
6350
|
-
|
|
6351
|
-
|
|
6352
|
-
|
|
6353
|
-
|
|
6354
|
-
|
|
6355
|
-
|
|
6356
|
-
|
|
6357
|
-
|
|
6358
|
-
|
|
6359
|
-
|
|
6360
|
-
|
|
6361
|
-
|
|
6362
|
-
|
|
6363
|
-
|
|
6364
|
-
|
|
6365
|
-
|
|
6366
|
-
|
|
6367
|
-
|
|
6368
|
-
|
|
6369
|
-
|
|
6370
|
-
|
|
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
|
|
6689
|
-
|
|
6690
|
-
|
|
6691
|
-
|
|
6692
|
-
|
|
6693
|
-
|
|
6694
|
-
|
|
6695
|
-
|
|
6696
|
-
|
|
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
|
|
6735
|
-
|
|
6736
|
-
|
|
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
|
-
|
|
6740
|
-
|
|
6741
|
-
|
|
6742
|
-
|
|
6743
|
-
|
|
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
|
|
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
|
-
|
|
6959
|
-
|
|
6960
|
-
|
|
6961
|
-
|
|
6962
|
-
|
|
6963
|
-
|
|
6964
|
-
|
|
6965
|
-
|
|
6966
|
-
|
|
6967
|
-
|
|
6968
|
-
|
|
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
|
|
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
|
-
|
|
6987
|
-
const
|
|
6988
|
-
|
|
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 {
|