@omnizap-system/omnizap 2.6.1 → 2.6.2
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/.env.example +54 -9
- package/.github/workflows/ci.yml +3 -3
- package/.github/workflows/security-runner-hardening.yml +1 -1
- package/.github/workflows/security-zap-full-scan.yml +1 -0
- package/app/config/index.js +2 -0
- package/app/configParts/adminIdentity.js +5 -5
- package/app/configParts/baileysConfig.js +226 -55
- package/app/configParts/groupUtils.js +5 -0
- package/app/configParts/messagePersistenceService.js +143 -3
- package/app/configParts/sessionConfig.js +157 -0
- package/app/connection/baileysCompatibility.test.js +1 -1
- package/app/connection/groupOwnerWriteStateResolver.js +109 -0
- package/app/connection/socketController.js +625 -124
- package/app/connection/socketController.multiSession.test.js +108 -0
- package/app/controllers/messageController.js +1 -1
- package/app/controllers/messagePipeline/commandMiddleware.js +12 -10
- package/app/controllers/messagePipeline/conversationMiddleware.js +2 -1
- package/app/controllers/messagePipeline/messagePipelineMiddlewares.test.js +104 -0
- package/app/controllers/messagePipeline/preProcessingMiddlewares.js +80 -2
- package/app/controllers/messageProcessingPipeline.js +88 -9
- package/app/controllers/messageProcessingPipeline.test.js +200 -0
- package/app/modules/adminModule/AGENT.md +1 -1
- package/app/modules/adminModule/commandConfig.json +3318 -1347
- package/app/modules/adminModule/groupCommandHandlers.js +856 -14
- package/app/modules/adminModule/groupCommandHandlers.test.js +375 -9
- package/app/modules/adminModule/groupWarningRepository.js +152 -0
- package/app/modules/aiModule/AGENT.md +47 -30
- package/app/modules/aiModule/aiConfigRuntime.js +1 -0
- package/app/modules/aiModule/catCommand.js +132 -25
- package/app/modules/aiModule/commandConfig.json +114 -28
- package/app/modules/analyticsModule/messageAnalysisEventRepository.js +54 -6
- package/app/modules/gameModule/AGENT.md +1 -1
- package/app/modules/gameModule/commandConfig.json +29 -0
- package/app/modules/menuModule/AGENT.md +1 -1
- package/app/modules/menuModule/commandConfig.json +45 -10
- package/app/modules/menuModule/menuCatalogService.js +190 -0
- package/app/modules/menuModule/menuCommandUsageRepository.js +109 -0
- package/app/modules/menuModule/menuDynamicService.js +511 -0
- package/app/modules/menuModule/menuDynamicService.test.js +141 -0
- package/app/modules/menuModule/menus.js +36 -5
- package/app/modules/playModule/AGENT.md +10 -5
- package/app/modules/playModule/commandConfig.json +74 -16
- package/app/modules/playModule/playCommandConstants.js +13 -7
- package/app/modules/playModule/playCommandCore.js +4 -6
- package/app/modules/playModule/{playCommandYtDlpClient.js → playCommandMediaClient.js} +684 -332
- package/app/modules/playModule/playConfigRuntime.js +5 -6
- package/app/modules/playModule/playModuleCriticalFlows.test.js +44 -59
- package/app/modules/quoteModule/AGENT.md +1 -1
- package/app/modules/quoteModule/commandConfig.json +29 -0
- package/app/modules/rpgPokemonModule/AGENT.md +1 -1
- package/app/modules/rpgPokemonModule/commandConfig.json +29 -0
- package/app/modules/statsModule/AGENT.md +1 -1
- package/app/modules/statsModule/commandConfig.json +58 -0
- package/app/modules/stickerModule/AGENT.md +1 -1
- package/app/modules/stickerModule/commandConfig.json +145 -0
- package/app/modules/stickerPackModule/AGENT.md +1 -1
- package/app/modules/stickerPackModule/autoPackCollectorService.js +5 -1
- package/app/modules/stickerPackModule/commandConfig.json +29 -0
- package/app/modules/stickerPackModule/stickerAutoPackByTagsRuntime.js +1 -1
- package/app/modules/stickerPackModule/stickerPackCommandHandlers.js +78 -57
- package/app/modules/stickerPackModule/stickerPackService.js +13 -6
- package/app/modules/systemMetricsModule/AGENT.md +1 -1
- package/app/modules/systemMetricsModule/commandConfig.json +29 -0
- package/app/modules/tiktokModule/AGENT.md +1 -1
- package/app/modules/tiktokModule/commandConfig.json +29 -0
- package/app/modules/userModule/AGENT.md +1 -1
- package/app/modules/userModule/commandConfig.json +29 -0
- package/app/modules/waifuPicsModule/AGENT.md +57 -27
- package/app/modules/waifuPicsModule/commandConfig.json +87 -0
- package/app/observability/metrics.js +136 -0
- package/app/services/ai/commandConfigEnrichmentService.js +229 -47
- package/app/services/ai/geminiService.js +131 -7
- package/app/services/ai/geminiService.test.js +59 -2
- package/app/services/ai/moduleAiHelpCoreService.js +33 -4
- package/app/services/group/groupMetadataService.js +24 -1
- package/app/services/infra/dbWriteQueue.js +51 -21
- package/app/services/messaging/newsBroadcastService.js +843 -27
- package/app/services/multiSession/assignmentBalancerService.js +457 -0
- package/app/services/multiSession/groupOwnershipRepository.js +381 -0
- package/app/services/multiSession/groupOwnershipService.js +890 -0
- package/app/services/multiSession/groupOwnershipService.test.js +309 -0
- package/app/services/multiSession/sessionRegistryService.js +293 -0
- package/app/store/aiPromptStore.js +36 -19
- package/app/store/groupConfigStore.js +41 -5
- package/app/store/premiumUserStore.js +21 -7
- package/app/utils/antiLink/antiLinkModule.js +352 -16
- package/app/workers/aiHelperContinuousLearningWorker.js +512 -0
- package/database/index.js +6 -0
- package/database/migrations/20260307_d0_hardening_down.sql +1 -1
- package/database/migrations/20260314_d7_canonical_sender_down.sql +1 -1
- package/database/migrations/20260406_d30_security_analytics_down.sql +1 -1
- package/database/migrations/20260411_d35_group_community_metadata_down.sql +59 -0
- package/database/migrations/20260411_d35_group_community_metadata_up.sql +62 -0
- package/database/migrations/20260412_d36_system_config_tables_down.sql +32 -0
- package/database/migrations/20260412_d36_system_config_tables_up.sql +66 -0
- package/database/migrations/20260413_d37_group_user_warnings_down.sql +11 -0
- package/database/migrations/20260413_d37_group_user_warnings_up.sql +24 -0
- package/database/migrations/20260414_d38_multi_session_foundation_down.sql +72 -0
- package/database/migrations/20260414_d38_multi_session_foundation_up.sql +125 -0
- package/database/migrations/20260414_d39_multi_session_cutover_down.sql +103 -0
- package/database/migrations/20260414_d39_multi_session_cutover_up.sql +83 -0
- package/database/schema.sql +102 -1
- package/docker-compose.yml +4 -1
- package/docs/compliance/acceptable-use-policy-2026-03-07.md +1 -1
- package/docs/compliance/privacy-policy-2026-03-07.md +2 -2
- package/docs/security/dsar-lgpd-runbook-2026-03-07.md +1 -1
- package/docs/security/network-hardening-runbook-2026-03-07.md +53 -0
- package/docs/security/omnizap-static-security-headers.conf +25 -0
- package/ecosystem.prod.config.cjs +31 -11
- package/index.js +52 -18
- package/observability/alert-rules.yml +20 -0
- package/observability/grafana/dashboards/omnizap-system-admin.json +229 -0
- package/observability/mysql-setup.sql +4 -4
- package/observability/system-admin-observability.md +26 -0
- package/package.json +12 -5
- package/public/comandos/commands-catalog.json +2253 -78
- package/public/js/apps/commandsReactApp.js +267 -87
- package/public/js/apps/createPackApp.js +3 -3
- package/public/js/apps/stickersApp.js +255 -103
- package/public/js/apps/termsReactApp.js +57 -8
- package/public/js/apps/userPasswordResetReactApp.js +406 -0
- package/public/js/apps/userReactApp.js +96 -47
- package/public/js/apps/userSystemAdmReactApp.js +1506 -0
- package/public/pages/politica-de-privacidade.html +1 -1
- package/public/pages/stickers.html +5 -5
- package/public/pages/termos-de-uso-texto-integral.html +1 -1
- package/public/pages/termos-de-uso.html +1 -1
- package/public/pages/user-password-reset.html +3 -4
- package/public/pages/user-systemadm.html +8 -462
- package/public/pages/user.html +1 -1
- package/scripts/clear-whatsapp-session.sh +123 -0
- package/scripts/core-ai-mode.mjs +163 -0
- package/scripts/deploy.sh +10 -0
- package/scripts/enrich-command-config-ux-openai.mjs +492 -0
- package/scripts/generate-commands-catalog.mjs +155 -0
- package/scripts/new-whatsapp-session.sh +317 -0
- package/scripts/security-web-surface-check.mjs +218 -0
- package/server/controllers/admin/adminPanelHandlers.js +253 -3
- package/server/controllers/admin/systemAdminController.js +267 -0
- package/server/controllers/sticker/stickerCatalogController.js +9 -23
- package/server/controllers/system/contactController.js +9 -17
- package/server/controllers/system/stickerCatalogSystemContext.js +27 -6
- package/server/controllers/system/systemController.js +254 -1
- package/server/controllers/userController.js +6 -0
- package/server/email/emailTemplateService.js +3 -2
- package/server/http/httpServer.js +8 -4
- package/server/middleware/securityHeaders.js +20 -1
- package/server/routes/admin/systemAdminRouter.js +6 -0
- package/server/routes/indexRouter.js +30 -6
- package/server/routes/observability/grafanaProxyRouter.js +254 -0
- package/server/routes/static/staticPageRouter.js +27 -1
- package/server/utils/publicContact.js +31 -0
- package/utils/whatsapp/contactEnv.js +39 -0
- package/vite.config.mjs +2 -1
- package/app/modules/playModule/local/installYtDlp.js +0 -25
- package/app/modules/playModule/local/ytDlpInstaller.js +0 -28
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { now as __timeNow, nowIso as __timeNowIso, toUnixMs as __timeNowMs } from '#time';
|
|
2
2
|
import { randomUUID, randomBytes, scryptSync, timingSafeEqual } from 'node:crypto';
|
|
3
|
-
import { URLSearchParams } from 'node:url';
|
|
3
|
+
import { URL, URLSearchParams } from 'node:url';
|
|
4
4
|
|
|
5
|
+
import { setAdminOverviewSnapshot } from '../../../app/observability/metrics.js';
|
|
5
6
|
import { parseAdminModeratorUpsertPayload, parseAdminSessionPasswordPayload } from '../../auth/validation/authSchemas.js';
|
|
6
7
|
|
|
7
8
|
export const createStickerCatalogAdminHandlers = ({ executeQuery, tables, logger, sendJson, readJsonBody, parseCookies, getCookieValuesFromRequest, appendSetCookie, buildCookieString, sanitizeText, normalizeGoogleSubject, normalizeEmail, normalizeJid, toIsoOrNull, toWhatsAppPhoneDigits, mapGoogleSessionResponseData, resolveGoogleWebSessionFromRequest, revokeGoogleWebSessionsByIdentity, getMarketplaceGlobalStatsCached, getSystemSummaryCached, getFeatureFlagsSnapshot, refreshFeatureFlags, listAdminBans, createAdminBanRecord, revokeAdminBanRecord, normalizeVisitPath, stickerWebPath, findStickerPackByPackKey, stickerPackService, buildManagedPackResponseData, sendManagedMutationStatus, sendManagedPackMutationStatus, deleteManagedPackWithCleanup, mapStickerPackWebManageError, cleanupOrphanStickerAssets, invalidateStickerCatalogDerivedCaches }) => {
|
|
@@ -16,6 +17,229 @@ export const createStickerCatalogAdminHandlers = ({ executeQuery, tables, logger
|
|
|
16
17
|
const ADMIN_PANEL_SESSION_TTL_MS = Math.max(10 * 60 * 1000, Number(process.env.ADM_PANEL_SESSION_TTL_MS) || 12 * 60 * 60 * 1000);
|
|
17
18
|
const ADMIN_MODERATOR_PASSWORD_MIN_LENGTH = Math.max(6, Number(process.env.ADM_MODERATOR_PASSWORD_MIN_LENGTH) || 8);
|
|
18
19
|
const ADMIN_PANEL_SESSION_COOKIE_NAME = 'omnizap_admin_panel_session';
|
|
20
|
+
const SYSTEM_ADMIN_GRAFANA_TIME_FROM = sanitizeText(process.env.SYSTEM_ADMIN_GRAFANA_TIME_FROM || 'now-6h', 40, { allowEmpty: true }) || 'now-6h';
|
|
21
|
+
const SYSTEM_ADMIN_GRAFANA_TIME_TO = sanitizeText(process.env.SYSTEM_ADMIN_GRAFANA_TIME_TO || 'now', 40, { allowEmpty: true }) || 'now';
|
|
22
|
+
const SYSTEM_ADMIN_GRAFANA_REFRESH = sanitizeText(process.env.SYSTEM_ADMIN_GRAFANA_REFRESH || '10s', 20, { allowEmpty: true }) || '10s';
|
|
23
|
+
const SYSTEM_ADMIN_GRAFANA_STATUS_TIMEOUT_MS = Math.max(800, Number(process.env.SYSTEM_ADMIN_GRAFANA_STATUS_TIMEOUT_MS) || 2500);
|
|
24
|
+
const SYSTEM_ADMIN_GRAFANA_STATUS_CACHE_TTL_MS = Math.max(5000, Number(process.env.SYSTEM_ADMIN_GRAFANA_STATUS_CACHE_TTL_MS) || 15000);
|
|
25
|
+
|
|
26
|
+
const normalizeGrafanaBaseUrl = (value) => {
|
|
27
|
+
const raw = String(value || '').trim();
|
|
28
|
+
if (!raw) return '';
|
|
29
|
+
try {
|
|
30
|
+
const parsed = new URL(raw);
|
|
31
|
+
if (!['http:', 'https:'].includes(parsed.protocol)) return '';
|
|
32
|
+
const normalizedPathname = String(parsed.pathname || '/').replace(/\/+$/, '');
|
|
33
|
+
parsed.search = '';
|
|
34
|
+
parsed.hash = '';
|
|
35
|
+
return `${parsed.origin}${normalizedPathname === '/' ? '' : normalizedPathname}`;
|
|
36
|
+
} catch {
|
|
37
|
+
return '';
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const normalizeGrafanaUid = (value) =>
|
|
42
|
+
String(value || '')
|
|
43
|
+
.trim()
|
|
44
|
+
.replace(/[^a-zA-Z0-9_-]/g, '')
|
|
45
|
+
.slice(0, 120);
|
|
46
|
+
|
|
47
|
+
const slugifyGrafanaDashboard = (value) => {
|
|
48
|
+
const raw = String(value || '')
|
|
49
|
+
.trim()
|
|
50
|
+
.toLowerCase();
|
|
51
|
+
const slug = raw
|
|
52
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
53
|
+
.replace(/^-+|-+$/g, '')
|
|
54
|
+
.slice(0, 90);
|
|
55
|
+
return slug || 'dashboard';
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const parseGrafanaDashboardDefinitions = () => {
|
|
59
|
+
const raw = String(process.env.SYSTEM_ADMIN_GRAFANA_DASHBOARDS || '')
|
|
60
|
+
.split(',')
|
|
61
|
+
.map((item) => String(item || '').trim())
|
|
62
|
+
.filter(Boolean);
|
|
63
|
+
|
|
64
|
+
const parsed = raw
|
|
65
|
+
.map((entry) => {
|
|
66
|
+
const [uidPart, ...titleParts] = entry.split('|');
|
|
67
|
+
const uid = normalizeGrafanaUid(uidPart);
|
|
68
|
+
const title = sanitizeText(titleParts.join('|') || '', 90, { allowEmpty: true }) || '';
|
|
69
|
+
return uid ? { uid, title } : null;
|
|
70
|
+
})
|
|
71
|
+
.filter(Boolean);
|
|
72
|
+
|
|
73
|
+
if (parsed.length) return parsed;
|
|
74
|
+
|
|
75
|
+
return [
|
|
76
|
+
{ uid: 'omnizap-system-admin', title: 'System Admin' },
|
|
77
|
+
{ uid: 'omnizap-overview', title: 'Overview' },
|
|
78
|
+
{ uid: 'omnizap-mysql', title: 'MySQL' },
|
|
79
|
+
];
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const buildAdminGrafanaObservabilityLinks = () => {
|
|
83
|
+
const baseUrl = normalizeGrafanaBaseUrl(process.env.SYSTEM_ADMIN_GRAFANA_URL || process.env.GRAFANA_PUBLIC_URL || '');
|
|
84
|
+
if (!baseUrl) {
|
|
85
|
+
return {
|
|
86
|
+
enabled: false,
|
|
87
|
+
base_url: null,
|
|
88
|
+
dashboards: [],
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const base = new URL(`${baseUrl}/`);
|
|
93
|
+
const definitions = parseGrafanaDashboardDefinitions();
|
|
94
|
+
|
|
95
|
+
const dashboards = definitions
|
|
96
|
+
.map((entry, index) => {
|
|
97
|
+
const uid = normalizeGrafanaUid(entry?.uid || '');
|
|
98
|
+
if (!uid) return null;
|
|
99
|
+
const title = sanitizeText(entry?.title || '', 90, { allowEmpty: true }) || `Dashboard ${index + 1}`;
|
|
100
|
+
const slug = slugifyGrafanaDashboard(title || uid);
|
|
101
|
+
const viewUrl = new URL(`d/${encodeURIComponent(uid)}/${encodeURIComponent(slug)}`, base);
|
|
102
|
+
viewUrl.searchParams.set('orgId', '1');
|
|
103
|
+
viewUrl.searchParams.set('from', SYSTEM_ADMIN_GRAFANA_TIME_FROM);
|
|
104
|
+
viewUrl.searchParams.set('to', SYSTEM_ADMIN_GRAFANA_TIME_TO);
|
|
105
|
+
viewUrl.searchParams.set('refresh', SYSTEM_ADMIN_GRAFANA_REFRESH);
|
|
106
|
+
|
|
107
|
+
const embedUrl = new URL(String(viewUrl));
|
|
108
|
+
embedUrl.searchParams.set('kiosk', 'tv');
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
uid,
|
|
112
|
+
title,
|
|
113
|
+
view_url: viewUrl.toString(),
|
|
114
|
+
embed_url: embedUrl.toString(),
|
|
115
|
+
};
|
|
116
|
+
})
|
|
117
|
+
.filter(Boolean);
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
enabled: dashboards.length > 0,
|
|
121
|
+
base_url: baseUrl,
|
|
122
|
+
dashboards,
|
|
123
|
+
};
|
|
124
|
+
};
|
|
125
|
+
const grafanaObservabilityLinks = buildAdminGrafanaObservabilityLinks();
|
|
126
|
+
let grafanaRuntimeSnapshotCache = {
|
|
127
|
+
expires_at_ms: 0,
|
|
128
|
+
payload: null,
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const resolveGrafanaHealthEndpointUrl = () => {
|
|
132
|
+
const candidates = [process.env.GRAFANA_PROXY_TARGET_URL, process.env.GRAFANA_INTERNAL_URL, process.env.SYSTEM_ADMIN_GRAFANA_URL, process.env.GRAFANA_PUBLIC_URL];
|
|
133
|
+
for (const candidate of candidates) {
|
|
134
|
+
const baseUrl = normalizeGrafanaBaseUrl(candidate);
|
|
135
|
+
if (!baseUrl) continue;
|
|
136
|
+
try {
|
|
137
|
+
return new URL('api/health', `${baseUrl}/`).toString();
|
|
138
|
+
} catch {
|
|
139
|
+
// noop
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return '';
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const getGrafanaRuntimeSnapshot = async () => {
|
|
146
|
+
const nowMs = __timeNowMs();
|
|
147
|
+
if (grafanaRuntimeSnapshotCache.payload && nowMs < grafanaRuntimeSnapshotCache.expires_at_ms) {
|
|
148
|
+
return grafanaRuntimeSnapshotCache.payload;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const dashboardsTotal = Array.isArray(grafanaObservabilityLinks?.dashboards) ? grafanaObservabilityLinks.dashboards.length : 0;
|
|
152
|
+
const baseSnapshot = {
|
|
153
|
+
enabled: Boolean(grafanaObservabilityLinks?.enabled),
|
|
154
|
+
available: false,
|
|
155
|
+
status: 'unavailable',
|
|
156
|
+
response_ms: null,
|
|
157
|
+
checked_at: __timeNowIso(),
|
|
158
|
+
http_status: null,
|
|
159
|
+
version: null,
|
|
160
|
+
database: null,
|
|
161
|
+
commit: null,
|
|
162
|
+
dashboards_total: dashboardsTotal,
|
|
163
|
+
error: null,
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const healthEndpointUrl = resolveGrafanaHealthEndpointUrl();
|
|
167
|
+
if (!healthEndpointUrl) {
|
|
168
|
+
grafanaRuntimeSnapshotCache = {
|
|
169
|
+
expires_at_ms: nowMs + SYSTEM_ADMIN_GRAFANA_STATUS_CACHE_TTL_MS,
|
|
170
|
+
payload: baseSnapshot,
|
|
171
|
+
};
|
|
172
|
+
return baseSnapshot;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const controller = typeof AbortController === 'function' ? new AbortController() : null;
|
|
176
|
+
const timeoutId =
|
|
177
|
+
controller && Number.isFinite(SYSTEM_ADMIN_GRAFANA_STATUS_TIMEOUT_MS)
|
|
178
|
+
? setTimeout(() => {
|
|
179
|
+
controller.abort();
|
|
180
|
+
}, SYSTEM_ADMIN_GRAFANA_STATUS_TIMEOUT_MS)
|
|
181
|
+
: null;
|
|
182
|
+
const startedAtMs = __timeNowMs();
|
|
183
|
+
|
|
184
|
+
try {
|
|
185
|
+
const response = await globalThis.fetch(healthEndpointUrl, {
|
|
186
|
+
method: 'GET',
|
|
187
|
+
headers: {
|
|
188
|
+
Accept: 'application/json',
|
|
189
|
+
},
|
|
190
|
+
signal: controller?.signal,
|
|
191
|
+
});
|
|
192
|
+
const responseMs = Math.max(0, __timeNowMs() - startedAtMs);
|
|
193
|
+
let payload = null;
|
|
194
|
+
try {
|
|
195
|
+
payload = await response.json();
|
|
196
|
+
} catch {
|
|
197
|
+
payload = null;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const version = sanitizeText(payload?.version || '', 40, { allowEmpty: true }) || null;
|
|
201
|
+
const commit = sanitizeText(payload?.commit || '', 80, { allowEmpty: true }) || null;
|
|
202
|
+
const database = sanitizeText(payload?.database || '', 30, { allowEmpty: true }) || null;
|
|
203
|
+
const normalizedDatabase = String(database || '')
|
|
204
|
+
.trim()
|
|
205
|
+
.toLowerCase();
|
|
206
|
+
const status = !response.ok ? 'offline' : normalizedDatabase === 'ok' ? 'online' : normalizedDatabase ? 'degraded' : 'online';
|
|
207
|
+
|
|
208
|
+
const snapshot = {
|
|
209
|
+
...baseSnapshot,
|
|
210
|
+
available: response.ok,
|
|
211
|
+
status,
|
|
212
|
+
response_ms: responseMs,
|
|
213
|
+
checked_at: __timeNowIso(),
|
|
214
|
+
http_status: Number(response.status || 0) || null,
|
|
215
|
+
version,
|
|
216
|
+
database,
|
|
217
|
+
commit,
|
|
218
|
+
};
|
|
219
|
+
grafanaRuntimeSnapshotCache = {
|
|
220
|
+
expires_at_ms: __timeNowMs() + SYSTEM_ADMIN_GRAFANA_STATUS_CACHE_TTL_MS,
|
|
221
|
+
payload: snapshot,
|
|
222
|
+
};
|
|
223
|
+
return snapshot;
|
|
224
|
+
} catch (error) {
|
|
225
|
+
const responseMs = Math.max(0, __timeNowMs() - startedAtMs);
|
|
226
|
+
const isAbort = error?.name === 'AbortError';
|
|
227
|
+
const snapshot = {
|
|
228
|
+
...baseSnapshot,
|
|
229
|
+
status: 'offline',
|
|
230
|
+
response_ms: responseMs,
|
|
231
|
+
checked_at: __timeNowIso(),
|
|
232
|
+
error: sanitizeText(isAbort ? 'timeout' : error?.message || 'fetch_failed', 80, { allowEmpty: true }) || 'fetch_failed',
|
|
233
|
+
};
|
|
234
|
+
grafanaRuntimeSnapshotCache = {
|
|
235
|
+
expires_at_ms: __timeNowMs() + SYSTEM_ADMIN_GRAFANA_STATUS_CACHE_TTL_MS,
|
|
236
|
+
payload: snapshot,
|
|
237
|
+
};
|
|
238
|
+
return snapshot;
|
|
239
|
+
} finally {
|
|
240
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
241
|
+
}
|
|
242
|
+
};
|
|
19
243
|
|
|
20
244
|
const adminPanelSessionMap = new Map();
|
|
21
245
|
let adminPanelSessionPruneAt = 0;
|
|
@@ -957,7 +1181,7 @@ export const createStickerCatalogAdminHandlers = ({ executeQuery, tables, logger
|
|
|
957
1181
|
};
|
|
958
1182
|
|
|
959
1183
|
const buildAdminOverviewPayload = async ({ adminSession = null } = {}) => {
|
|
960
|
-
const [marketplaceStats, activeSessions, knownUsers, bans, packsCountRows, stickersCountRows, recentPacks, visitSummary, systemSummaryPayload, messageFlowDaily, moderationQueue, auditLog, featureFlags] = await Promise.all([
|
|
1184
|
+
const [marketplaceStats, activeSessions, knownUsers, bans, packsCountRows, stickersCountRows, recentPacks, visitSummary, systemSummaryPayload, messageFlowDaily, moderationQueue, auditLog, featureFlags, grafanaRuntimeSnapshot] = await Promise.all([
|
|
961
1185
|
getMarketplaceGlobalStatsCached().catch(() => null),
|
|
962
1186
|
listAdminActiveGoogleWebSessions({ limit: 80 }),
|
|
963
1187
|
listAdminKnownGoogleUsers({ limit: 120 }),
|
|
@@ -976,6 +1200,19 @@ export const createStickerCatalogAdminHandlers = ({ executeQuery, tables, logger
|
|
|
976
1200
|
buildModerationQueueSnapshot({ limit: 80 }).catch(() => []),
|
|
977
1201
|
listAdminAuditLog({ limit: 120 }).catch(() => []),
|
|
978
1202
|
listAdminFeatureFlagsDetailed({ limit: 300 }).catch(() => []),
|
|
1203
|
+
getGrafanaRuntimeSnapshot().catch(() => ({
|
|
1204
|
+
enabled: Boolean(grafanaObservabilityLinks?.enabled),
|
|
1205
|
+
available: false,
|
|
1206
|
+
status: 'offline',
|
|
1207
|
+
response_ms: null,
|
|
1208
|
+
checked_at: __timeNowIso(),
|
|
1209
|
+
http_status: null,
|
|
1210
|
+
version: null,
|
|
1211
|
+
database: null,
|
|
1212
|
+
commit: null,
|
|
1213
|
+
dashboards_total: Array.isArray(grafanaObservabilityLinks?.dashboards) ? grafanaObservabilityLinks.dashboards.length : 0,
|
|
1214
|
+
error: 'runtime_snapshot_failed',
|
|
1215
|
+
})),
|
|
979
1216
|
]);
|
|
980
1217
|
|
|
981
1218
|
const systemSummary = systemSummaryPayload?.data || null;
|
|
@@ -997,7 +1234,7 @@ export const createStickerCatalogAdminHandlers = ({ executeQuery, tables, logger
|
|
|
997
1234
|
systemMeta,
|
|
998
1235
|
});
|
|
999
1236
|
|
|
1000
|
-
|
|
1237
|
+
const overviewPayload = {
|
|
1001
1238
|
admin_session: mapAdminPanelSessionResponseData(adminSession),
|
|
1002
1239
|
marketplace_stats: marketplaceStats,
|
|
1003
1240
|
counters: {
|
|
@@ -1045,9 +1282,22 @@ export const createStickerCatalogAdminHandlers = ({ executeQuery, tables, logger
|
|
|
1045
1282
|
visit_metrics: visitSummary,
|
|
1046
1283
|
system_summary: systemSummary,
|
|
1047
1284
|
system_meta: systemMeta,
|
|
1285
|
+
observability_links: {
|
|
1286
|
+
grafana: grafanaObservabilityLinks,
|
|
1287
|
+
},
|
|
1288
|
+
observability_runtime: {
|
|
1289
|
+
grafana: grafanaRuntimeSnapshot,
|
|
1290
|
+
},
|
|
1048
1291
|
message_flow_daily: messageFlowDaily,
|
|
1049
1292
|
updated_at: __timeNowIso(),
|
|
1050
1293
|
};
|
|
1294
|
+
|
|
1295
|
+
setAdminOverviewSnapshot({
|
|
1296
|
+
overview: overviewPayload,
|
|
1297
|
+
source: 'admin_overview_payload',
|
|
1298
|
+
});
|
|
1299
|
+
|
|
1300
|
+
return overviewPayload;
|
|
1051
1301
|
};
|
|
1052
1302
|
|
|
1053
1303
|
const findAdminPackContextByKey = async (rawPackKey) => {
|
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { timingSafeEqual } from 'node:crypto';
|
|
3
4
|
import { URL } from 'node:url';
|
|
4
5
|
|
|
5
6
|
import logger from '#logger';
|
|
7
|
+
import {
|
|
8
|
+
forceSystemAdminGroupFailover,
|
|
9
|
+
listSystemAdminAssignmentHistory,
|
|
10
|
+
listSystemAdminAssignments,
|
|
11
|
+
listSystemAdminSessions,
|
|
12
|
+
setSystemAdminGroupPin,
|
|
13
|
+
triggerSystemAdminManualRebalance,
|
|
14
|
+
} from '../system/systemController.js';
|
|
6
15
|
|
|
7
16
|
const parseEnvBool = (value, fallback) => {
|
|
8
17
|
if (value === undefined || value === null || value === '') return fallback;
|
|
@@ -25,6 +34,8 @@ const SYSTEM_ADMIN_API_BASE_PATH = normalizeBasePath(process.env.SYSTEM_ADMIN_AP
|
|
|
25
34
|
const SYSTEM_ADMIN_API_SESSION_PATH = `${SYSTEM_ADMIN_API_BASE_PATH}/session`;
|
|
26
35
|
const LEGACY_SYSTEM_ADMIN_API_BASE_PATH = `${LEGACY_STICKER_API_BASE_PATH}/admin`;
|
|
27
36
|
const LEGACY_SYSTEM_ADMIN_API_SESSION_PATH = `${LEGACY_SYSTEM_ADMIN_API_BASE_PATH}/session`;
|
|
37
|
+
const SYSTEM_ADMIN_MULTI_SESSION_API_PATH = `${SYSTEM_ADMIN_API_BASE_PATH}/multi-session`;
|
|
38
|
+
const LEGACY_SYSTEM_ADMIN_MULTI_SESSION_API_PATH = `${LEGACY_SYSTEM_ADMIN_API_BASE_PATH}/multi-session`;
|
|
28
39
|
const STICKER_LOGIN_WEB_PATH = normalizeBasePath(process.env.STICKER_LOGIN_WEB_PATH, '/login');
|
|
29
40
|
const STICKER_WEB_PATH = normalizeBasePath(process.env.STICKER_WEB_PATH, '/stickers');
|
|
30
41
|
const STICKER_ADMIN_WEB_PATH = `${STICKER_WEB_PATH}/admin`;
|
|
@@ -37,6 +48,9 @@ const SITE_ORIGIN = String(process.env.SITE_ORIGIN || 'https://omnizap.shop')
|
|
|
37
48
|
|
|
38
49
|
const USER_SYSTEMADM_TEMPLATE_PATH = path.join(process.cwd(), 'public', 'pages', 'user-systemadm.html');
|
|
39
50
|
const LEGACY_STICKER_ADMIN_TEMPLATE_PATH = path.join(process.cwd(), 'public', 'pages', 'stickers-admin.html');
|
|
51
|
+
const SYSTEM_ADMIN_OPS_TOKEN = String(
|
|
52
|
+
process.env.SYSTEM_ADMIN_OPS_TOKEN || process.env.USER_INTERNAL_API_TOKEN || process.env.ADMIN_TOKEN || process.env.ADMIN_API_TOKEN || '',
|
|
53
|
+
).trim();
|
|
40
54
|
|
|
41
55
|
let stickerCatalogControllerPromise = null;
|
|
42
56
|
const loadStickerCatalogController = async () => {
|
|
@@ -108,6 +122,106 @@ const mapAdminApiPathToLegacy = (pathname) => {
|
|
|
108
122
|
return null;
|
|
109
123
|
};
|
|
110
124
|
|
|
125
|
+
const mapMultiSessionApiPath = (pathname) => {
|
|
126
|
+
if (hasPathPrefix(pathname, SYSTEM_ADMIN_MULTI_SESSION_API_PATH)) {
|
|
127
|
+
const suffix = pathname.slice(SYSTEM_ADMIN_MULTI_SESSION_API_PATH.length);
|
|
128
|
+
return suffix || '/';
|
|
129
|
+
}
|
|
130
|
+
if (hasPathPrefix(pathname, LEGACY_SYSTEM_ADMIN_MULTI_SESSION_API_PATH)) {
|
|
131
|
+
const suffix = pathname.slice(LEGACY_SYSTEM_ADMIN_MULTI_SESSION_API_PATH.length);
|
|
132
|
+
return suffix || '/';
|
|
133
|
+
}
|
|
134
|
+
return null;
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const constantTimeStringEqual = (left, right) => {
|
|
138
|
+
const leftBuffer = Buffer.from(String(left || ''), 'utf8');
|
|
139
|
+
const rightBuffer = Buffer.from(String(right || ''), 'utf8');
|
|
140
|
+
if (!leftBuffer.length || leftBuffer.length !== rightBuffer.length) return false;
|
|
141
|
+
try {
|
|
142
|
+
return timingSafeEqual(leftBuffer, rightBuffer);
|
|
143
|
+
} catch {
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const extractBearerToken = (req) => {
|
|
149
|
+
const authHeader = String(req?.headers?.authorization || '').trim();
|
|
150
|
+
if (!authHeader.toLowerCase().startsWith('bearer ')) return '';
|
|
151
|
+
return authHeader.slice(7).trim();
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const resolveOpsTokenFromRequest = (req) =>
|
|
155
|
+
String(req?.headers?.['x-system-admin-token'] || req?.headers?.['x-internal-api-token'] || req?.headers?.['x-admin-token'] || '').trim() ||
|
|
156
|
+
extractBearerToken(req);
|
|
157
|
+
|
|
158
|
+
const hasValidOpsToken = (req) => {
|
|
159
|
+
if (!SYSTEM_ADMIN_OPS_TOKEN) return true;
|
|
160
|
+
const requestToken = resolveOpsTokenFromRequest(req);
|
|
161
|
+
if (!requestToken) return false;
|
|
162
|
+
return constantTimeStringEqual(requestToken, SYSTEM_ADMIN_OPS_TOKEN);
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const readJsonBody = async (req, { maxBytes = 64 * 1024 } = {}) =>
|
|
166
|
+
new Promise((resolve, reject) => {
|
|
167
|
+
const chunks = [];
|
|
168
|
+
let total = 0;
|
|
169
|
+
|
|
170
|
+
req.on('data', (chunk) => {
|
|
171
|
+
total += chunk.length;
|
|
172
|
+
if (total > maxBytes) {
|
|
173
|
+
const error = new Error('Payload excedeu limite permitido.');
|
|
174
|
+
error.statusCode = 413;
|
|
175
|
+
reject(error);
|
|
176
|
+
req.destroy();
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
chunks.push(chunk);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
req.on('end', () => {
|
|
183
|
+
const raw = Buffer.concat(chunks).toString('utf8').trim();
|
|
184
|
+
if (!raw) {
|
|
185
|
+
resolve({});
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
try {
|
|
190
|
+
resolve(JSON.parse(raw));
|
|
191
|
+
} catch {
|
|
192
|
+
const error = new Error('JSON invalido.');
|
|
193
|
+
error.statusCode = 400;
|
|
194
|
+
reject(error);
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
req.on('error', (error) => reject(error));
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
const parseBool = (value, fallback = false) => {
|
|
202
|
+
if (value === undefined || value === null || value === '') return fallback;
|
|
203
|
+
const normalized = String(value).trim().toLowerCase();
|
|
204
|
+
if (['1', 'true', 'yes', 'y', 'on'].includes(normalized)) return true;
|
|
205
|
+
if (['0', 'false', 'no', 'n', 'off'].includes(normalized)) return false;
|
|
206
|
+
return fallback;
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
const parsePositiveInt = (value, fallback = 200, min = 1, max = 5000) => {
|
|
210
|
+
const parsed = Number.parseInt(String(value ?? ''), 10);
|
|
211
|
+
if (!Number.isFinite(parsed)) return fallback;
|
|
212
|
+
return Math.max(min, Math.min(max, parsed));
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
const decodePathSegment = (value) => {
|
|
216
|
+
const raw = String(value || '').trim();
|
|
217
|
+
if (!raw) return '';
|
|
218
|
+
try {
|
|
219
|
+
return decodeURIComponent(raw);
|
|
220
|
+
} catch {
|
|
221
|
+
return raw;
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
|
|
111
225
|
const renderUserSystemAdminHtml = async () => {
|
|
112
226
|
const template = await fs.readFile(USER_SYSTEMADM_TEMPLATE_PATH, 'utf8');
|
|
113
227
|
const dataAttributes = {
|
|
@@ -124,13 +238,147 @@ const renderUserSystemAdminHtml = async () => {
|
|
|
124
238
|
return html;
|
|
125
239
|
};
|
|
126
240
|
|
|
241
|
+
const requireSystemAdminOpsAccess = (req, res) => {
|
|
242
|
+
if (hasValidOpsToken(req)) return true;
|
|
243
|
+
sendJson(req, res, 401, { error: 'Nao autorizado para operacoes de system admin.' });
|
|
244
|
+
return false;
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
const normalizeMultiSessionSubPath = (value) => {
|
|
248
|
+
const raw = String(value || '/').trim();
|
|
249
|
+
if (!raw || raw === '/') return '/';
|
|
250
|
+
return `/${raw
|
|
251
|
+
.replace(/^\/+/g, '')
|
|
252
|
+
.replace(/\/+$/g, '')}`;
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
const handleMultiSessionOpsRequest = async (req, res, { pathname, url }) => {
|
|
256
|
+
if (!requireSystemAdminOpsAccess(req, res)) return true;
|
|
257
|
+
|
|
258
|
+
const subPath = normalizeMultiSessionSubPath(pathname);
|
|
259
|
+
const requestUrl = (() => {
|
|
260
|
+
try {
|
|
261
|
+
return new URL(String(url?.href || req.url || '/'), SITE_ORIGIN);
|
|
262
|
+
} catch {
|
|
263
|
+
return new URL(SITE_ORIGIN);
|
|
264
|
+
}
|
|
265
|
+
})();
|
|
266
|
+
|
|
267
|
+
if (subPath === '/sessions') {
|
|
268
|
+
if (!['GET', 'HEAD'].includes(req.method || '')) {
|
|
269
|
+
sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
|
|
270
|
+
return true;
|
|
271
|
+
}
|
|
272
|
+
const payload = await listSystemAdminSessions({
|
|
273
|
+
status: requestUrl.searchParams.get('status'),
|
|
274
|
+
limit: parsePositiveInt(requestUrl.searchParams.get('limit'), 200, 1, 5000),
|
|
275
|
+
});
|
|
276
|
+
sendJson(req, res, 200, payload);
|
|
277
|
+
return true;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (subPath === '/assignments') {
|
|
281
|
+
if (!['GET', 'HEAD'].includes(req.method || '')) {
|
|
282
|
+
sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
|
|
283
|
+
return true;
|
|
284
|
+
}
|
|
285
|
+
const payload = await listSystemAdminAssignments({
|
|
286
|
+
groupJid: requestUrl.searchParams.get('group_jid'),
|
|
287
|
+
ownerSessionId: requestUrl.searchParams.get('owner_session_id'),
|
|
288
|
+
includeExpired: parseBool(requestUrl.searchParams.get('include_expired'), false),
|
|
289
|
+
limit: parsePositiveInt(requestUrl.searchParams.get('limit'), 200, 1, 5000),
|
|
290
|
+
});
|
|
291
|
+
sendJson(req, res, 200, payload);
|
|
292
|
+
return true;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (subPath === '/history') {
|
|
296
|
+
if (!['GET', 'HEAD'].includes(req.method || '')) {
|
|
297
|
+
sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
|
|
298
|
+
return true;
|
|
299
|
+
}
|
|
300
|
+
const payload = await listSystemAdminAssignmentHistory({
|
|
301
|
+
groupJid: requestUrl.searchParams.get('group_jid'),
|
|
302
|
+
limit: parsePositiveInt(requestUrl.searchParams.get('limit'), 100, 1, 5000),
|
|
303
|
+
});
|
|
304
|
+
sendJson(req, res, 200, payload);
|
|
305
|
+
return true;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (subPath === '/rebalance') {
|
|
309
|
+
if (!['POST'].includes(req.method || '')) {
|
|
310
|
+
sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
|
|
311
|
+
return true;
|
|
312
|
+
}
|
|
313
|
+
const payload = await triggerSystemAdminManualRebalance();
|
|
314
|
+
sendJson(req, res, 200, payload);
|
|
315
|
+
return true;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const groupActionMatch = subPath.match(/^\/groups\/([^/]+)\/(pin|unpin|failover)$/i);
|
|
319
|
+
if (groupActionMatch) {
|
|
320
|
+
if (!['POST'].includes(req.method || '')) {
|
|
321
|
+
sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
|
|
322
|
+
return true;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const groupJid = decodePathSegment(groupActionMatch[1]);
|
|
326
|
+
const action = String(groupActionMatch[2] || '').toLowerCase();
|
|
327
|
+
const body = await readJsonBody(req).catch((error) => {
|
|
328
|
+
const statusCode = Number(error?.statusCode || 400);
|
|
329
|
+
sendJson(req, res, statusCode, { error: error?.message || 'Falha ao interpretar payload JSON.' });
|
|
330
|
+
return null;
|
|
331
|
+
});
|
|
332
|
+
if (body === null) return true;
|
|
333
|
+
|
|
334
|
+
if (action === 'pin' || action === 'unpin') {
|
|
335
|
+
const pinned = action === 'pin';
|
|
336
|
+
const payload = await setSystemAdminGroupPin({
|
|
337
|
+
groupJid,
|
|
338
|
+
pinned,
|
|
339
|
+
sessionId: body?.session_id || body?.sessionId || null,
|
|
340
|
+
reason: body?.reason || null,
|
|
341
|
+
changedBy: 'system_admin_api',
|
|
342
|
+
metadata: body?.metadata || null,
|
|
343
|
+
});
|
|
344
|
+
sendJson(req, res, 200, payload);
|
|
345
|
+
return true;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (action === 'failover') {
|
|
349
|
+
const targetSessionId = String(body?.target_session_id || body?.targetSessionId || requestUrl.searchParams.get('target_session_id') || '')
|
|
350
|
+
.trim()
|
|
351
|
+
.slice(0, 64);
|
|
352
|
+
if (!targetSessionId) {
|
|
353
|
+
sendJson(req, res, 400, { error: 'target_session_id e obrigatorio.' });
|
|
354
|
+
return true;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const payload = await forceSystemAdminGroupFailover({
|
|
358
|
+
groupJid,
|
|
359
|
+
targetSessionId,
|
|
360
|
+
reason: body?.reason || 'admin_force_failover',
|
|
361
|
+
changedBy: 'system_admin_api',
|
|
362
|
+
metadata: body?.metadata || null,
|
|
363
|
+
});
|
|
364
|
+
sendJson(req, res, 200, payload);
|
|
365
|
+
return true;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
sendJson(req, res, 404, { error: 'Endpoint de operacao multi-session nao encontrado.' });
|
|
370
|
+
return true;
|
|
371
|
+
};
|
|
372
|
+
|
|
127
373
|
export const getSystemAdminRouteConfig = () => ({
|
|
128
374
|
webPath: USER_SYSTEMADM_WEB_PATH,
|
|
129
375
|
legacyWebPath: STICKER_ADMIN_WEB_PATH,
|
|
130
376
|
apiAdminBasePath: SYSTEM_ADMIN_API_BASE_PATH,
|
|
131
377
|
apiAdminSessionPath: SYSTEM_ADMIN_API_SESSION_PATH,
|
|
378
|
+
apiAdminMultiSessionPath: SYSTEM_ADMIN_MULTI_SESSION_API_PATH,
|
|
132
379
|
legacyApiAdminBasePath: LEGACY_SYSTEM_ADMIN_API_BASE_PATH,
|
|
133
380
|
legacyApiAdminSessionPath: LEGACY_SYSTEM_ADMIN_API_SESSION_PATH,
|
|
381
|
+
legacyApiAdminMultiSessionPath: LEGACY_SYSTEM_ADMIN_MULTI_SESSION_API_PATH,
|
|
134
382
|
});
|
|
135
383
|
|
|
136
384
|
export const maybeHandleSystemAdminRequest = async (req, res, { pathname, url }) => {
|
|
@@ -186,6 +434,25 @@ export const maybeHandleSystemAdminRequest = async (req, res, { pathname, url })
|
|
|
186
434
|
}
|
|
187
435
|
|
|
188
436
|
if (hasPathPrefix(pathname, SYSTEM_ADMIN_API_BASE_PATH) || hasPathPrefix(pathname, LEGACY_SYSTEM_ADMIN_API_BASE_PATH)) {
|
|
437
|
+
const multiSessionPath = mapMultiSessionApiPath(pathname);
|
|
438
|
+
if (multiSessionPath !== null) {
|
|
439
|
+
try {
|
|
440
|
+
return await handleMultiSessionOpsRequest(req, res, {
|
|
441
|
+
pathname: multiSessionPath,
|
|
442
|
+
url,
|
|
443
|
+
});
|
|
444
|
+
} catch (error) {
|
|
445
|
+
logger.error('Falha ao processar endpoint operacional multi-sessao.', {
|
|
446
|
+
action: 'system_admin_multi_session_endpoint_failed',
|
|
447
|
+
method: req.method,
|
|
448
|
+
path: pathname,
|
|
449
|
+
error: error?.message,
|
|
450
|
+
});
|
|
451
|
+
sendJson(req, res, 500, { error: 'Falha interna ao processar operacao multi-sessao.' });
|
|
452
|
+
return true;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
189
456
|
const legacyPathname = mapAdminApiPathToLegacy(pathname);
|
|
190
457
|
if (!legacyPathname) return false;
|
|
191
458
|
|
|
@@ -67,6 +67,7 @@ export const stripWebpExtension = (value) =>
|
|
|
67
67
|
.replace(/\.webp$/i, '');
|
|
68
68
|
|
|
69
69
|
const clampInt = (value, fallback, min, max) => {
|
|
70
|
+
if (value === undefined || value === null || value === '') return fallback;
|
|
70
71
|
const parsed = Number(value);
|
|
71
72
|
if (!Number.isFinite(parsed)) return fallback;
|
|
72
73
|
return Math.max(min, Math.min(max, Math.floor(parsed)));
|
|
@@ -964,8 +965,6 @@ const convertUploadMediaToWebp = async ({ ownerJid, buffer, mimetype }) => {
|
|
|
964
965
|
const PACK_TAG_MARKER_REGEX = /\[pack-tags:([^\]]+)\]/i;
|
|
965
966
|
const AUTO_PACK_MARKER_REGEX = /\[(?:auto-theme|auto-tag):[^\]]+\]/gi;
|
|
966
967
|
const AUTO_PACK_MARKER_TEST_REGEX = /\[(?:auto-theme|auto-tag):[^\]]+\]/i;
|
|
967
|
-
const AUTO_PACK_COLLECTOR_MARKER = '[auto-pack:collector]';
|
|
968
|
-
const AUTO_PACK_COLLECTOR_LEGACY_TEXT = 'coleção automática de figurinhas criadas pelo usuário.';
|
|
969
968
|
const AUTO_PACK_DESCRIPTION_PREFIX_REGEX = /^curadoria automática por tema\.\s*tema:\s*[^.]+\.?\s*(?:score\s*=\s*-?\d+(?:\.\d+)?\.?\s*)?/i;
|
|
970
969
|
const AUTO_PACK_SCORE_FRAGMENT_REGEX = /\bscore\s*=\s*-?\d+(?:\.\d+)?\.?/gi;
|
|
971
970
|
const normalizePackTag = (value) =>
|
|
@@ -1024,29 +1023,10 @@ const parsePackDescriptionMetadata = (description) => {
|
|
|
1024
1023
|
};
|
|
1025
1024
|
};
|
|
1026
1025
|
|
|
1027
|
-
const isCollectorAutoPack = (pack) => {
|
|
1028
|
-
if (!pack || typeof pack !== 'object') return false;
|
|
1029
|
-
const description = String(pack.description || '').toLowerCase();
|
|
1030
|
-
return description.includes(AUTO_PACK_COLLECTOR_MARKER) || description.includes(AUTO_PACK_COLLECTOR_LEGACY_TEXT);
|
|
1031
|
-
};
|
|
1032
|
-
|
|
1033
|
-
const isThemeCurationAutoPack = (pack) => {
|
|
1034
|
-
if (!pack || typeof pack !== 'object') return false;
|
|
1035
|
-
const name = String(pack.name || '').trim();
|
|
1036
|
-
if (/^\[auto\]/i.test(name)) return true;
|
|
1037
|
-
|
|
1038
|
-
const description = String(pack.description || '').toLowerCase();
|
|
1039
|
-
if (description.includes('[auto-theme:') || description.includes('[auto-tag:')) return true;
|
|
1040
|
-
|
|
1041
|
-
return Boolean(String(pack.pack_theme_key || '').trim());
|
|
1042
|
-
};
|
|
1043
|
-
|
|
1044
1026
|
const shouldHidePackFromMyProfileDefault = (pack, { includeAutoPacks = false } = {}) => {
|
|
1045
1027
|
if (!pack || typeof pack !== 'object') return false;
|
|
1046
1028
|
if (includeAutoPacks) return false;
|
|
1047
|
-
|
|
1048
|
-
if (isThemeCurationAutoPack(pack)) return true;
|
|
1049
|
-
return pack.is_auto_pack === true || Number(pack.is_auto_pack || 0) === 1;
|
|
1029
|
+
return false;
|
|
1050
1030
|
};
|
|
1051
1031
|
|
|
1052
1032
|
const buildPackDescriptionWithTags = (description, tags = []) => {
|
|
@@ -1734,6 +1714,12 @@ const getMarketplaceStatsCached = async (visibility) => {
|
|
|
1734
1714
|
bucket.expiresAt = __timeNowMs() + HOME_MARKETPLACE_STATS_CACHE_SECONDS * 1000;
|
|
1735
1715
|
return data;
|
|
1736
1716
|
})
|
|
1717
|
+
.catch((error) => {
|
|
1718
|
+
if (hasValue && bucket.value) {
|
|
1719
|
+
return bucket.value;
|
|
1720
|
+
}
|
|
1721
|
+
throw error;
|
|
1722
|
+
})
|
|
1737
1723
|
.finally(() => {
|
|
1738
1724
|
bucket.pending = null;
|
|
1739
1725
|
});
|
|
@@ -3219,7 +3205,7 @@ const handleCreatePackRequest = async (req, res) => {
|
|
|
3219
3205
|
});
|
|
3220
3206
|
const manualTags = mergeUniqueTags(Array.isArray(payload?.tags) ? payload.tags : []).slice(0, 8);
|
|
3221
3207
|
const persistedDescription = buildPackDescriptionWithTags(description, manualTags);
|
|
3222
|
-
const visibility = String(payload?.visibility || '
|
|
3208
|
+
const visibility = String(payload?.visibility || 'private')
|
|
3223
3209
|
.trim()
|
|
3224
3210
|
.toLowerCase();
|
|
3225
3211
|
const googleSession = await resolveGoogleWebSessionFromRequest(req);
|