@omnizap-system/omnizap 2.6.1 → 2.6.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/.env.example +78 -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 +6 -0
- package/app/configParts/adminIdentity.js +36 -7
- package/app/configParts/baileysConfig.js +343 -56
- package/app/configParts/groupUtils.js +226 -0
- package/app/configParts/loggerConfig.js +185 -0
- package/app/configParts/messagePersistenceService.js +307 -5
- package/app/configParts/sessionConfig.js +242 -0
- package/app/connection/baileysCompatibility.test.js +10 -1
- package/app/connection/baileysDbAuthState.js +205 -9
- package/app/connection/baileysLibsignalPatch.js +210 -0
- package/app/connection/groupOwnerWriteStateResolver.js +141 -0
- package/app/connection/socketController.js +694 -123
- package/app/connection/socketController.multiSession.test.js +128 -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 +96 -4
- package/app/controllers/messageProcessingPipeline.js +90 -9
- package/app/controllers/messageProcessingPipeline.test.js +202 -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 +452 -0
- package/app/services/multiSession/groupOwnershipRepository.js +346 -0
- package/app/services/multiSession/groupOwnershipService.js +809 -0
- package/app/services/multiSession/groupOwnershipService.test.js +317 -0
- package/app/services/multiSession/sessionRegistryService.js +239 -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 +391 -25
- 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 +14 -6
- package/public/comandos/commands-catalog.json +2253 -78
- package/public/css/payments-react.css +478 -0
- package/public/js/apps/commandsReactApp.js +267 -87
- package/public/js/apps/createPackApp.js +3 -3
- package/public/js/apps/homeReactApp.js +2 -2
- package/public/js/apps/paymentsCancelReactApp.js +45 -0
- package/public/js/apps/paymentsReactApp.js +399 -0
- package/public/js/apps/paymentsSuccessReactApp.js +148 -0
- 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/pagamentos-cancelado.html +21 -0
- package/public/pages/pagamentos-sucesso.html +21 -0
- package/public/pages/pagamentos.html +30 -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 +13 -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 +564 -0
- package/scripts/security-web-surface-check.mjs +218 -0
- package/server/controllers/admin/adminPanelHandlers.js +253 -3
- package/server/controllers/admin/systemAdminController.js +254 -0
- package/server/controllers/payments/paymentsController.js +731 -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 +228 -1
- package/server/controllers/userController.js +6 -0
- package/server/email/emailAutomationRuntime.js +36 -1
- package/server/email/emailAutomationService.js +42 -1
- package/server/email/emailTemplateService.js +140 -33
- package/server/http/httpRequestUtils.js +18 -14
- package/server/http/httpServer.js +8 -4
- package/server/middleware/securityHeaders.js +35 -3
- package/server/routes/admin/systemAdminRouter.js +6 -0
- package/server/routes/indexRouter.js +50 -6
- package/server/routes/observability/grafanaProxyRouter.js +254 -0
- package/server/routes/payments/paymentsRouter.js +47 -0
- package/server/routes/static/staticPageRouter.js +30 -1
- package/server/utils/publicContact.js +31 -0
- package/utils/whatsapp/contactEnv.js +39 -0
- package/vite.config.mjs +5 -1
- package/app/modules/playModule/local/installYtDlp.js +0 -25
- package/app/modules/playModule/local/ytDlpInstaller.js +0 -28
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { nowIso as __timeNowIso } from '#time';
|
|
4
|
+
|
|
5
|
+
import fs from 'node:fs/promises';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import process from 'node:process';
|
|
8
|
+
|
|
9
|
+
const rawBaseUrl = String(process.env.SECURITY_WEB_SURFACE_BASE_URL || 'https://omnizap.shop').trim() || 'https://omnizap.shop';
|
|
10
|
+
const reportPath = String(process.env.SECURITY_WEB_SURFACE_REPORT_PATH || './temp/security-web-surface-report.json').trim();
|
|
11
|
+
const requestTimeoutMs = Math.max(1_000, Number(process.env.SECURITY_WEB_SURFACE_TIMEOUT_MS || 10_000));
|
|
12
|
+
|
|
13
|
+
const toBaseOrigin = (value) => {
|
|
14
|
+
const raw = String(value || '').trim();
|
|
15
|
+
if (!raw) return 'https://omnizap.shop';
|
|
16
|
+
try {
|
|
17
|
+
const parsed = new URL(raw);
|
|
18
|
+
return parsed.origin;
|
|
19
|
+
} catch {
|
|
20
|
+
try {
|
|
21
|
+
const parsed = new URL(`https://${raw}`);
|
|
22
|
+
return parsed.origin;
|
|
23
|
+
} catch {
|
|
24
|
+
return 'https://omnizap.shop';
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const baseOrigin = toBaseOrigin(rawBaseUrl);
|
|
30
|
+
|
|
31
|
+
const PASS = 'PASS';
|
|
32
|
+
const FAIL = 'FAIL';
|
|
33
|
+
|
|
34
|
+
const STATIC_REQUIRED_HEADERS = ['content-security-policy', 'permissions-policy', 'strict-transport-security', 'x-content-type-options', 'x-frame-options'];
|
|
35
|
+
|
|
36
|
+
const API_REQUIRED_HEADERS = ['content-security-policy', 'cross-origin-opener-policy', 'cross-origin-resource-policy', 'permissions-policy', 'strict-transport-security', 'x-content-type-options', 'x-frame-options'];
|
|
37
|
+
|
|
38
|
+
const checks = [
|
|
39
|
+
{
|
|
40
|
+
id: 1,
|
|
41
|
+
name: 'Root static page has security headers',
|
|
42
|
+
path: '/',
|
|
43
|
+
expectedStatuses: [200],
|
|
44
|
+
requiredHeaders: STATIC_REQUIRED_HEADERS,
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
id: 2,
|
|
48
|
+
name: 'Legal page has security headers',
|
|
49
|
+
path: '/notice-and-takedown/',
|
|
50
|
+
expectedStatuses: [200],
|
|
51
|
+
requiredHeaders: STATIC_REQUIRED_HEADERS,
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
id: 3,
|
|
55
|
+
name: 'Dotenv path is not exposed',
|
|
56
|
+
path: '/.env',
|
|
57
|
+
forbiddenStatuses: [200],
|
|
58
|
+
bodyLeakPatterns: [/DB_PASSWORD|MYSQL_PASSWORD|GITHUB_TOKEN|SECRET|PRIVATE_KEY/i],
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
id: 4,
|
|
62
|
+
name: 'Unknown path does not soft-fallback with 200',
|
|
63
|
+
path: '/__security_probe_nonexistent_omnizap__.txt',
|
|
64
|
+
expectedStatuses: [404],
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
id: 5,
|
|
68
|
+
name: 'Whitespace path fuzz does not return 200',
|
|
69
|
+
path: '/assets%20/',
|
|
70
|
+
forbiddenStatuses: [200],
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
id: 6,
|
|
74
|
+
name: 'API bootstrap keeps hardened headers',
|
|
75
|
+
path: '/api/home-bootstrap',
|
|
76
|
+
expectedStatuses: [200],
|
|
77
|
+
requiredHeaders: API_REQUIRED_HEADERS,
|
|
78
|
+
},
|
|
79
|
+
];
|
|
80
|
+
|
|
81
|
+
const request = async (targetPath) => {
|
|
82
|
+
const normalizedPath = String(targetPath || '/').startsWith('/') ? String(targetPath || '/') : `/${String(targetPath || '/')}`;
|
|
83
|
+
const url = `${baseOrigin}${normalizedPath}`;
|
|
84
|
+
const controller = new AbortController();
|
|
85
|
+
const timeout = setTimeout(() => controller.abort(), requestTimeoutMs);
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
const response = await fetch(url, {
|
|
89
|
+
method: 'GET',
|
|
90
|
+
redirect: 'manual',
|
|
91
|
+
signal: controller.signal,
|
|
92
|
+
});
|
|
93
|
+
const text = await response.text();
|
|
94
|
+
return {
|
|
95
|
+
ok: true,
|
|
96
|
+
url,
|
|
97
|
+
status: response.status,
|
|
98
|
+
headers: Object.fromEntries(response.headers.entries()),
|
|
99
|
+
body: text,
|
|
100
|
+
};
|
|
101
|
+
} catch (error) {
|
|
102
|
+
return {
|
|
103
|
+
ok: false,
|
|
104
|
+
url,
|
|
105
|
+
status: null,
|
|
106
|
+
headers: {},
|
|
107
|
+
body: '',
|
|
108
|
+
error: error?.message || String(error),
|
|
109
|
+
};
|
|
110
|
+
} finally {
|
|
111
|
+
clearTimeout(timeout);
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const summarize = (items) =>
|
|
116
|
+
items.reduce(
|
|
117
|
+
(acc, item) => {
|
|
118
|
+
acc[item.status] = (acc[item.status] || 0) + 1;
|
|
119
|
+
return acc;
|
|
120
|
+
},
|
|
121
|
+
{ PASS: 0, FAIL: 0 },
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
const runCheck = async (check) => {
|
|
125
|
+
const response = await request(check.path);
|
|
126
|
+
const reasons = [];
|
|
127
|
+
|
|
128
|
+
if (!response.ok) {
|
|
129
|
+
reasons.push(`network_error:${response.error || 'unknown'}`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (Array.isArray(check.expectedStatuses) && check.expectedStatuses.length > 0) {
|
|
133
|
+
if (!check.expectedStatuses.includes(response.status)) {
|
|
134
|
+
reasons.push(`unexpected_status:${response.status}`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (Array.isArray(check.forbiddenStatuses) && check.forbiddenStatuses.length > 0) {
|
|
139
|
+
if (check.forbiddenStatuses.includes(response.status)) {
|
|
140
|
+
reasons.push(`forbidden_status:${response.status}`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const missingHeaders = [];
|
|
145
|
+
for (const headerName of check.requiredHeaders || []) {
|
|
146
|
+
const resolved = response.headers?.[headerName];
|
|
147
|
+
if (!resolved) missingHeaders.push(headerName);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (missingHeaders.length > 0) {
|
|
151
|
+
reasons.push(`missing_headers:${missingHeaders.join(',')}`);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const leakPatternHit = (check.bodyLeakPatterns || []).find((pattern) => pattern.test(String(response.body || '')));
|
|
155
|
+
if (leakPatternHit) {
|
|
156
|
+
reasons.push(`body_leak_pattern:${String(leakPatternHit)}`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
id: check.id,
|
|
161
|
+
name: check.name,
|
|
162
|
+
path: check.path,
|
|
163
|
+
status: reasons.length > 0 ? FAIL : PASS,
|
|
164
|
+
reasons,
|
|
165
|
+
evidence: {
|
|
166
|
+
url: response.url,
|
|
167
|
+
status: response.status,
|
|
168
|
+
headers: response.headers,
|
|
169
|
+
body_preview: String(response.body || '').slice(0, 240),
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
const writeReport = async (payload) => {
|
|
175
|
+
const absolutePath = path.resolve(process.cwd(), reportPath);
|
|
176
|
+
await fs.mkdir(path.dirname(absolutePath), { recursive: true });
|
|
177
|
+
await fs.writeFile(absolutePath, `${JSON.stringify(payload, null, 2)}\n`, 'utf8');
|
|
178
|
+
return absolutePath;
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const main = async () => {
|
|
182
|
+
const startedAt = __timeNowIso();
|
|
183
|
+
const results = [];
|
|
184
|
+
|
|
185
|
+
for (const check of checks) {
|
|
186
|
+
const result = await runCheck(check);
|
|
187
|
+
results.push(result);
|
|
188
|
+
console.log(`[security-web-surface] ${String(result.id).padStart(2, '0')} ${result.name}: ${result.status}`);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const summary = summarize(results);
|
|
192
|
+
const endedAt = __timeNowIso();
|
|
193
|
+
const report = {
|
|
194
|
+
meta: {
|
|
195
|
+
base_origin: baseOrigin,
|
|
196
|
+
started_at: startedAt,
|
|
197
|
+
ended_at: endedAt,
|
|
198
|
+
timeout_ms: requestTimeoutMs,
|
|
199
|
+
},
|
|
200
|
+
summary,
|
|
201
|
+
results,
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
const reportAbsolutePath = await writeReport(report);
|
|
205
|
+
console.log('[security-web-surface] ---');
|
|
206
|
+
console.log(`[security-web-surface] base_origin=${baseOrigin}`);
|
|
207
|
+
console.log(`[security-web-surface] summary=${JSON.stringify(summary)}`);
|
|
208
|
+
console.log(`[security-web-surface] report_path=${reportAbsolutePath}`);
|
|
209
|
+
|
|
210
|
+
if ((summary.FAIL || 0) > 0) {
|
|
211
|
+
process.exitCode = 1;
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
main().catch((error) => {
|
|
216
|
+
console.error(`[security-web-surface] fatal_error=${error?.message || error}`);
|
|
217
|
+
process.exit(1);
|
|
218
|
+
});
|
|
@@ -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) => {
|