@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
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import http from 'node:http';
|
|
2
|
+
import https from 'node:https';
|
|
3
|
+
import { URL } from 'node:url';
|
|
4
|
+
|
|
5
|
+
import logger from '#logger';
|
|
6
|
+
|
|
7
|
+
const HOP_BY_HOP_HEADERS = new Set(['connection', 'keep-alive', 'proxy-authenticate', 'proxy-authorization', 'te', 'trailer', 'transfer-encoding', 'upgrade']);
|
|
8
|
+
|
|
9
|
+
const DEFAULT_GRAFANA_PROXY_BASE_PATH = '/api/grafana';
|
|
10
|
+
const DEFAULT_GRAFANA_PROXY_LEGACY_BASE_PATH = '/grafana';
|
|
11
|
+
const DEFAULT_GRAFANA_PROXY_TIMEOUT_MS = 15000;
|
|
12
|
+
|
|
13
|
+
const normalizeBasePath = (value, fallback) => {
|
|
14
|
+
const raw = String(value || '').trim() || fallback;
|
|
15
|
+
const withLeadingSlash = raw.startsWith('/') ? raw : `/${raw}`;
|
|
16
|
+
const withoutTrailingSlash = withLeadingSlash.length > 1 && withLeadingSlash.endsWith('/') ? withLeadingSlash.slice(0, -1) : withLeadingSlash;
|
|
17
|
+
return withoutTrailingSlash || fallback;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const startsWithPath = (pathname, prefix) => {
|
|
21
|
+
if (!pathname || !prefix) return false;
|
|
22
|
+
if (pathname === prefix) return true;
|
|
23
|
+
return pathname.startsWith(`${prefix}/`);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const parseEnvBool = (value, fallback = true) => {
|
|
27
|
+
if (value === undefined || value === null || value === '') return fallback;
|
|
28
|
+
const normalized = String(value).trim().toLowerCase();
|
|
29
|
+
if (['1', 'true', 'yes', 'y', 'on'].includes(normalized)) return true;
|
|
30
|
+
if (['0', 'false', 'no', 'n', 'off'].includes(normalized)) return false;
|
|
31
|
+
return fallback;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const parseEnvNumber = (value, fallback) => {
|
|
35
|
+
const parsed = Number(value);
|
|
36
|
+
return Number.isFinite(parsed) && parsed > 0 ? Math.round(parsed) : fallback;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const normalizeTargetUrl = (rawUrl) => {
|
|
40
|
+
const raw = String(rawUrl || '').trim();
|
|
41
|
+
if (!raw) return null;
|
|
42
|
+
try {
|
|
43
|
+
const parsed = new URL(raw);
|
|
44
|
+
if (!['http:', 'https:'].includes(parsed.protocol)) return null;
|
|
45
|
+
parsed.search = '';
|
|
46
|
+
parsed.hash = '';
|
|
47
|
+
parsed.pathname = String(parsed.pathname || '/').replace(/\/+$/, '') || '/';
|
|
48
|
+
return parsed;
|
|
49
|
+
} catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const normalizeHeaderValue = (value) => {
|
|
55
|
+
if (Array.isArray(value))
|
|
56
|
+
return value
|
|
57
|
+
.map((item) => String(item || '').trim())
|
|
58
|
+
.filter(Boolean)
|
|
59
|
+
.join(', ');
|
|
60
|
+
return String(value || '').trim();
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const getDefaultTargetUrl = () => {
|
|
64
|
+
const port = parseEnvNumber(process.env.GRAFANA_PORT, 3003);
|
|
65
|
+
return `http://127.0.0.1:${port}`;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const joinUrlPath = (leftPath, rightPath) => {
|
|
69
|
+
const left = String(leftPath || '').replace(/\/+$/, '');
|
|
70
|
+
const right = String(rightPath || '').replace(/^\/+/, '');
|
|
71
|
+
if (!left) return `/${right}`.replace(/\/{2,}/g, '/');
|
|
72
|
+
if (!right) return left.startsWith('/') ? left : `/${left}`;
|
|
73
|
+
const joined = `${left}/${right}`.replace(/\/{2,}/g, '/');
|
|
74
|
+
return joined.startsWith('/') ? joined : `/${joined}`;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const buildUpstreamPath = ({ pathname, search = '', matchedBasePath, upstreamBasePath, canonicalBasePath }) => {
|
|
78
|
+
const suffix = pathname === matchedBasePath ? '' : pathname.slice(matchedBasePath.length);
|
|
79
|
+
const normalizedSuffix = suffix ? (suffix.startsWith('/') ? suffix : `/${suffix}`) : '';
|
|
80
|
+
const canonicalPath = `${canonicalBasePath}${normalizedSuffix}` || canonicalBasePath;
|
|
81
|
+
const path = joinUrlPath(upstreamBasePath === '/' ? '' : upstreamBasePath, canonicalPath);
|
|
82
|
+
return `${path}${search || ''}`;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const getMatchedBasePath = (pathname, config) => {
|
|
86
|
+
if (!config?.enabled) return null;
|
|
87
|
+
if (startsWithPath(pathname, config.basePath)) return config.basePath;
|
|
88
|
+
if (config.legacyBasePath && startsWithPath(pathname, config.legacyBasePath)) return config.legacyBasePath;
|
|
89
|
+
return null;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const copyResponseHeaders = (res, upstreamHeaders = {}) => {
|
|
93
|
+
for (const [name, value] of Object.entries(upstreamHeaders)) {
|
|
94
|
+
if (value === undefined) continue;
|
|
95
|
+
const lower = String(name || '').toLowerCase();
|
|
96
|
+
if (!lower || HOP_BY_HOP_HEADERS.has(lower)) continue;
|
|
97
|
+
res.setHeader(name, value);
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const buildUpstreamHeaders = (req, { matchedBasePath, target }) => {
|
|
102
|
+
const headers = {};
|
|
103
|
+
for (const [name, value] of Object.entries(req.headers || {})) {
|
|
104
|
+
if (value === undefined) continue;
|
|
105
|
+
const lower = String(name || '').toLowerCase();
|
|
106
|
+
if (!lower || lower === 'host' || HOP_BY_HOP_HEADERS.has(lower)) continue;
|
|
107
|
+
headers[lower] = value;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const incomingForwardedFor = normalizeHeaderValue(req.headers?.['x-forwarded-for']);
|
|
111
|
+
const remoteAddress = String(req.socket?.remoteAddress || '').trim();
|
|
112
|
+
const forwardedFor = [incomingForwardedFor, remoteAddress].filter(Boolean).join(', ');
|
|
113
|
+
if (forwardedFor) headers['x-forwarded-for'] = forwardedFor;
|
|
114
|
+
|
|
115
|
+
const forwardedProto = normalizeHeaderValue(req.headers?.['x-forwarded-proto']) || (req.socket?.encrypted ? 'https' : 'http');
|
|
116
|
+
headers['x-forwarded-proto'] = forwardedProto;
|
|
117
|
+
|
|
118
|
+
const incomingHost = normalizeHeaderValue(req.headers?.host);
|
|
119
|
+
const forwardedHost = normalizeHeaderValue(req.headers?.['x-forwarded-host']) || incomingHost;
|
|
120
|
+
if (forwardedHost) headers['x-forwarded-host'] = forwardedHost;
|
|
121
|
+
|
|
122
|
+
headers['x-forwarded-prefix'] = matchedBasePath;
|
|
123
|
+
headers.host = incomingHost || target.host;
|
|
124
|
+
return headers;
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const pipeToUpstream = ({ req, res, target, targetPathWithQuery, timeoutMs, headers }) =>
|
|
128
|
+
new Promise((resolve, reject) => {
|
|
129
|
+
let settled = false;
|
|
130
|
+
const finalize = (error = null) => {
|
|
131
|
+
if (settled) return;
|
|
132
|
+
settled = true;
|
|
133
|
+
if (error) reject(error);
|
|
134
|
+
else resolve();
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const transport = target.protocol === 'https:' ? https : http;
|
|
138
|
+
const upstreamReq = transport.request(
|
|
139
|
+
{
|
|
140
|
+
protocol: target.protocol,
|
|
141
|
+
hostname: target.hostname,
|
|
142
|
+
port: target.port || (target.protocol === 'https:' ? 443 : 80),
|
|
143
|
+
method: req.method,
|
|
144
|
+
path: targetPathWithQuery,
|
|
145
|
+
headers,
|
|
146
|
+
},
|
|
147
|
+
(upstreamRes) => {
|
|
148
|
+
res.statusCode = Number(upstreamRes.statusCode) || 502;
|
|
149
|
+
if (upstreamRes.statusMessage) res.statusMessage = upstreamRes.statusMessage;
|
|
150
|
+
copyResponseHeaders(res, upstreamRes.headers);
|
|
151
|
+
|
|
152
|
+
if (String(req.method || '').toUpperCase() === 'HEAD') {
|
|
153
|
+
upstreamRes.resume();
|
|
154
|
+
upstreamRes.once('end', () => {
|
|
155
|
+
if (!res.writableEnded) res.end();
|
|
156
|
+
finalize();
|
|
157
|
+
});
|
|
158
|
+
upstreamRes.once('error', finalize);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
upstreamRes.once('error', finalize);
|
|
163
|
+
res.once('error', finalize);
|
|
164
|
+
upstreamRes.pipe(res);
|
|
165
|
+
upstreamRes.once('end', finalize);
|
|
166
|
+
},
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
upstreamReq.setTimeout(timeoutMs, () => {
|
|
170
|
+
upstreamReq.destroy(new Error(`Grafana proxy timeout (${timeoutMs}ms)`));
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
upstreamReq.once('error', finalize);
|
|
174
|
+
req.once('error', (error) => upstreamReq.destroy(error));
|
|
175
|
+
req.once('aborted', () => upstreamReq.destroy(new Error('Client aborted request')));
|
|
176
|
+
|
|
177
|
+
if (['GET', 'HEAD'].includes(String(req.method || '').toUpperCase())) {
|
|
178
|
+
upstreamReq.end();
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
req.pipe(upstreamReq);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
export const getGrafanaProxyRouterConfig = () => {
|
|
186
|
+
const enabled = parseEnvBool(process.env.GRAFANA_PROXY_ENABLED, true);
|
|
187
|
+
const basePath = normalizeBasePath(process.env.GRAFANA_PROXY_BASE_PATH, DEFAULT_GRAFANA_PROXY_BASE_PATH);
|
|
188
|
+
const legacyBasePath = normalizeBasePath(process.env.GRAFANA_PROXY_LEGACY_BASE_PATH, DEFAULT_GRAFANA_PROXY_LEGACY_BASE_PATH);
|
|
189
|
+
const timeoutMs = parseEnvNumber(process.env.GRAFANA_PROXY_TIMEOUT_MS, DEFAULT_GRAFANA_PROXY_TIMEOUT_MS);
|
|
190
|
+
const target = normalizeTargetUrl(process.env.GRAFANA_PROXY_TARGET_URL || getDefaultTargetUrl());
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
enabled: Boolean(enabled && target),
|
|
194
|
+
basePath,
|
|
195
|
+
legacyBasePath,
|
|
196
|
+
timeoutMs,
|
|
197
|
+
target,
|
|
198
|
+
};
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
export const shouldHandleGrafanaProxyPath = (pathname, config = null) => Boolean(getMatchedBasePath(pathname, config || getGrafanaProxyRouterConfig()));
|
|
202
|
+
|
|
203
|
+
const sendProxyError = (req, res, message) => {
|
|
204
|
+
if (res.writableEnded) return;
|
|
205
|
+
res.statusCode = 502;
|
|
206
|
+
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
207
|
+
if (req.method === 'HEAD') {
|
|
208
|
+
res.end();
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
res.end(JSON.stringify({ error: message }));
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
export const maybeHandleGrafanaProxyRequest = async (req, res, { pathname, url, config = null } = {}) => {
|
|
215
|
+
const resolvedConfig = config || getGrafanaProxyRouterConfig();
|
|
216
|
+
const matchedBasePath = getMatchedBasePath(pathname, resolvedConfig);
|
|
217
|
+
if (!matchedBasePath) return false;
|
|
218
|
+
|
|
219
|
+
if (!resolvedConfig?.target) {
|
|
220
|
+
sendProxyError(req, res, 'Grafana proxy indisponivel.');
|
|
221
|
+
return true;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const targetPathWithQuery = buildUpstreamPath({
|
|
225
|
+
pathname,
|
|
226
|
+
search: url?.search || '',
|
|
227
|
+
matchedBasePath,
|
|
228
|
+
upstreamBasePath: resolvedConfig.target.pathname || '/',
|
|
229
|
+
canonicalBasePath: resolvedConfig.basePath,
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
try {
|
|
233
|
+
const headers = buildUpstreamHeaders(req, { matchedBasePath, target: resolvedConfig.target });
|
|
234
|
+
await pipeToUpstream({
|
|
235
|
+
req,
|
|
236
|
+
res,
|
|
237
|
+
target: resolvedConfig.target,
|
|
238
|
+
targetPathWithQuery,
|
|
239
|
+
timeoutMs: resolvedConfig.timeoutMs,
|
|
240
|
+
headers,
|
|
241
|
+
});
|
|
242
|
+
} catch (error) {
|
|
243
|
+
logger.warn('Falha ao encaminhar request para o Grafana.', {
|
|
244
|
+
action: 'grafana_proxy_failed',
|
|
245
|
+
path: pathname,
|
|
246
|
+
method: req.method,
|
|
247
|
+
target_path: targetPathWithQuery,
|
|
248
|
+
error: error?.message,
|
|
249
|
+
});
|
|
250
|
+
sendProxyError(req, res, 'Grafana indisponivel no momento.');
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return true;
|
|
254
|
+
};
|
|
@@ -2,6 +2,7 @@ import fs from 'node:fs/promises';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
|
|
4
4
|
import logger from '#logger';
|
|
5
|
+
import { buildWhatsappUrl, formatWhatsappDisplay, resolvePublicWhatsappNumber } from '../../utils/publicContact.js';
|
|
5
6
|
|
|
6
7
|
const normalizeBasePath = (value, fallback) => {
|
|
7
8
|
const raw = String(value || '').trim() || fallback;
|
|
@@ -21,6 +22,11 @@ const PUBLIC_PAGES_DIR = path.join(process.cwd(), 'public', 'pages');
|
|
|
21
22
|
const SEO_ROUTE_PREFIX = '/seo/';
|
|
22
23
|
const SEO_SLUG_REGEX = /^[a-z0-9-]+$/;
|
|
23
24
|
const INDEX_FILE_SUFFIX = '/index.html';
|
|
25
|
+
const LGPD_DEFAULT_TEXT = 'Olá, gostaria de exercer meus direitos de titular de dados (LGPD).';
|
|
26
|
+
const SUPPORT_WHATSAPP_NUMBER = resolvePublicWhatsappNumber();
|
|
27
|
+
const SUPPORT_WHATSAPP_URL = buildWhatsappUrl(SUPPORT_WHATSAPP_NUMBER);
|
|
28
|
+
const SUPPORT_WHATSAPP_URL_LGPD = buildWhatsappUrl(SUPPORT_WHATSAPP_NUMBER, process.env.WHATSAPP_SUPPORT_LGPD_TEXT || LGPD_DEFAULT_TEXT);
|
|
29
|
+
const SUPPORT_WHATSAPP_DISPLAY = formatWhatsappDisplay(SUPPORT_WHATSAPP_NUMBER);
|
|
24
30
|
|
|
25
31
|
const STATIC_PAGE_ROUTE_TO_FILE = new Map(
|
|
26
32
|
[
|
|
@@ -89,6 +95,26 @@ const sendJson = (req, res, statusCode, payload) => {
|
|
|
89
95
|
res.end(body);
|
|
90
96
|
};
|
|
91
97
|
|
|
98
|
+
const injectStaticTemplateTokens = (html) => {
|
|
99
|
+
let rendered = String(html || '');
|
|
100
|
+
const replacements = {
|
|
101
|
+
__WHATSAPP_SUPPORT_NUMBER__: SUPPORT_WHATSAPP_NUMBER,
|
|
102
|
+
__WHATSAPP_SUPPORT_URL__: SUPPORT_WHATSAPP_URL,
|
|
103
|
+
__WHATSAPP_SUPPORT_URL_LGPD__: SUPPORT_WHATSAPP_URL_LGPD,
|
|
104
|
+
__WHATSAPP_SUPPORT_DISPLAY__: SUPPORT_WHATSAPP_DISPLAY,
|
|
105
|
+
__WHATSAPP_PUBLIC_CONTACT_NUMBER__: SUPPORT_WHATSAPP_NUMBER,
|
|
106
|
+
__WHATSAPP_PUBLIC_CONTACT_URL__: SUPPORT_WHATSAPP_URL,
|
|
107
|
+
__WHATSAPP_PUBLIC_CONTACT_URL_LGPD__: SUPPORT_WHATSAPP_URL_LGPD,
|
|
108
|
+
__WHATSAPP_PUBLIC_CONTACT_DISPLAY__: SUPPORT_WHATSAPP_DISPLAY,
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
for (const [token, value] of Object.entries(replacements)) {
|
|
112
|
+
rendered = rendered.replaceAll(token, String(value || ''));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return rendered;
|
|
116
|
+
};
|
|
117
|
+
|
|
92
118
|
export const shouldHandleStaticPagePath = (pathname) => Boolean(resolveStaticTemplateName(pathname));
|
|
93
119
|
|
|
94
120
|
export const maybeHandleStaticPageRequest = async (req, res, { pathname } = {}) => {
|
|
@@ -116,7 +142,7 @@ export const maybeHandleStaticPageRequest = async (req, res, { pathname } = {})
|
|
|
116
142
|
const templatePath = path.join(PUBLIC_PAGES_DIR, templateName);
|
|
117
143
|
try {
|
|
118
144
|
const html = await fs.readFile(templatePath, 'utf8');
|
|
119
|
-
sendHtml(req, res, html);
|
|
145
|
+
sendHtml(req, res, injectStaticTemplateTokens(html));
|
|
120
146
|
} catch (error) {
|
|
121
147
|
if (error?.code === 'ENOENT') {
|
|
122
148
|
sendJson(req, res, 404, { error: 'Template da pagina nao encontrado.' });
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { isLikelyWhatsAppPhone, normalizePhoneDigits, resolveSupportPhoneFromEnv } from '../../utils/whatsapp/contactEnv.js';
|
|
2
|
+
|
|
3
|
+
const FALLBACK_WHATSAPP_SUPPORT_NUMBER = '5511999999999';
|
|
4
|
+
|
|
5
|
+
export { normalizePhoneDigits };
|
|
6
|
+
|
|
7
|
+
export const resolvePublicWhatsappNumber = ({ fallback = FALLBACK_WHATSAPP_SUPPORT_NUMBER } = {}) => resolveSupportPhoneFromEnv({ fallback });
|
|
8
|
+
|
|
9
|
+
export const formatWhatsappDisplay = (value) => {
|
|
10
|
+
const digits = normalizePhoneDigits(value || '');
|
|
11
|
+
if (!isLikelyWhatsAppPhone(digits)) return '';
|
|
12
|
+
|
|
13
|
+
if (digits.startsWith('55') && digits.length === 12) {
|
|
14
|
+
return `+55 ${digits.slice(2, 4)} ${digits.slice(4, 8)}-${digits.slice(8)}`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (digits.startsWith('55') && digits.length === 13) {
|
|
18
|
+
return `+55 ${digits.slice(2, 4)} ${digits.slice(4, 9)}-${digits.slice(9)}`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return `+${digits}`;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const buildWhatsappUrl = (value, text = '') => {
|
|
25
|
+
const digits = normalizePhoneDigits(value || '');
|
|
26
|
+
if (!isLikelyWhatsAppPhone(digits)) return '';
|
|
27
|
+
|
|
28
|
+
const normalizedText = String(text || '').trim();
|
|
29
|
+
if (!normalizedText) return `https://wa.me/${digits}`;
|
|
30
|
+
return `https://wa.me/${digits}?text=${encodeURIComponent(normalizedText)}`;
|
|
31
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
const MAX_PHONE_DIGITS = 20;
|
|
2
|
+
const DEFAULT_FALLBACK_NUMBER = '5511999999999';
|
|
3
|
+
|
|
4
|
+
const BOT_PHONE_ENV_KEYS = ['WHATSAPP_BOT_NUMBER', 'WHATSAPP_SUPPORT_NUMBER', 'BOT_NUMBER', 'BOT_PHONE_NUMBER', 'PHONE_NUMBER'];
|
|
5
|
+
const SUPPORT_PHONE_ENV_KEYS = ['WHATSAPP_SUPPORT_NUMBER', 'WHATSAPP_ADMIN_NUMBER', 'WHATSAPP_PUBLIC_CONTACT_NUMBER', 'EMAIL_BRAND_SUPPORT_PHONE', 'OWNER_NUMBER'];
|
|
6
|
+
const ADMIN_PHONE_ENV_KEYS = ['WHATSAPP_ADMIN_NUMBER', 'WHATSAPP_ADMIN_JID', 'USER_ADMIN', 'OWNER_NUMBER', 'WHATSAPP_SUPPORT_NUMBER', 'WHATSAPP_PUBLIC_CONTACT_NUMBER'];
|
|
7
|
+
const ADMIN_IDENTITY_ENV_KEYS = ['WHATSAPP_ADMIN_JID', 'USER_ADMIN', 'WHATSAPP_ADMIN_NUMBER'];
|
|
8
|
+
|
|
9
|
+
export const normalizePhoneDigits = (value, maxLength = MAX_PHONE_DIGITS) =>
|
|
10
|
+
String(value || '')
|
|
11
|
+
.replace(/\D+/g, '')
|
|
12
|
+
.slice(0, Math.max(1, Number(maxLength) || MAX_PHONE_DIGITS));
|
|
13
|
+
|
|
14
|
+
export const isLikelyWhatsAppPhone = (value) => /^\d{10,15}$/.test(normalizePhoneDigits(value, 20));
|
|
15
|
+
|
|
16
|
+
const resolvePhoneFromEnvKeys = (keys, { fallback = '' } = {}) => {
|
|
17
|
+
for (const envKey of keys) {
|
|
18
|
+
const digits = normalizePhoneDigits(process.env[envKey] || '');
|
|
19
|
+
if (isLikelyWhatsAppPhone(digits)) return digits;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const fallbackDigits = normalizePhoneDigits(fallback || '');
|
|
23
|
+
if (isLikelyWhatsAppPhone(fallbackDigits)) return fallbackDigits;
|
|
24
|
+
return '';
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const resolveBotPhoneFromEnv = ({ fallback = DEFAULT_FALLBACK_NUMBER } = {}) => resolvePhoneFromEnvKeys(BOT_PHONE_ENV_KEYS, { fallback });
|
|
28
|
+
|
|
29
|
+
export const resolveSupportPhoneFromEnv = ({ fallback = DEFAULT_FALLBACK_NUMBER } = {}) => resolvePhoneFromEnvKeys(SUPPORT_PHONE_ENV_KEYS, { fallback });
|
|
30
|
+
|
|
31
|
+
export const resolveAdminPhoneFromEnv = ({ fallback = DEFAULT_FALLBACK_NUMBER } = {}) => resolvePhoneFromEnvKeys(ADMIN_PHONE_ENV_KEYS, { fallback });
|
|
32
|
+
|
|
33
|
+
export const resolveAdminIdentityRawFromEnv = ({ fallback = '' } = {}) => {
|
|
34
|
+
for (const envKey of ADMIN_IDENTITY_ENV_KEYS) {
|
|
35
|
+
const value = String(process.env[envKey] || '').trim();
|
|
36
|
+
if (value) return value;
|
|
37
|
+
}
|
|
38
|
+
return String(fallback || '').trim();
|
|
39
|
+
};
|
package/vite.config.mjs
CHANGED
|
@@ -24,6 +24,7 @@ export default defineConfig({
|
|
|
24
24
|
input: {
|
|
25
25
|
'home-react': path.join(projectRoot, 'public', 'js', 'apps', 'homeReactApp.js'),
|
|
26
26
|
'login-react': path.join(projectRoot, 'public', 'js', 'apps', 'loginReactApp.js'),
|
|
27
|
+
'user-password-reset-react': path.join(projectRoot, 'public', 'js', 'apps', 'userPasswordResetReactApp.js'),
|
|
27
28
|
'user-react': path.join(projectRoot, 'public', 'js', 'apps', 'userReactApp.js'),
|
|
28
29
|
'commands-react': path.join(projectRoot, 'public', 'js', 'apps', 'commandsReactApp.js'),
|
|
29
30
|
'terms-react': path.join(projectRoot, 'public', 'js', 'apps', 'termsReactApp.js'),
|
|
@@ -31,7 +32,7 @@ export default defineConfig({
|
|
|
31
32
|
'stickers-react': path.join(projectRoot, 'public', 'js', 'apps', 'stickersApp.js'),
|
|
32
33
|
'create-pack-react': path.join(projectRoot, 'public', 'js', 'apps', 'createPackApp.js'),
|
|
33
34
|
'stickers-admin': path.join(projectRoot, 'public', 'js', 'apps', 'stickersAdminApp.js'),
|
|
34
|
-
'user-systemadm': path.join(projectRoot, 'public', 'js', 'apps', '
|
|
35
|
+
'user-systemadm': path.join(projectRoot, 'public', 'js', 'apps', 'userSystemAdmReactApp.js'),
|
|
35
36
|
},
|
|
36
37
|
output: {
|
|
37
38
|
format: 'es',
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import { installYtDlpBinary, DEFAULT_YTDLP_BINARY_PATH } from './ytDlpInstaller.js';
|
|
2
|
-
import { fileURLToPath } from 'node:url';
|
|
3
|
-
|
|
4
|
-
async function instalarYtDlp() {
|
|
5
|
-
console.log('📥 Iniciando instalação do yt-dlp...');
|
|
6
|
-
console.log('⬇️ Baixando yt-dlp (versão mais recente)...');
|
|
7
|
-
|
|
8
|
-
const caminhoBinario = await installYtDlpBinary({
|
|
9
|
-
binaryPath: DEFAULT_YTDLP_BINARY_PATH,
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
console.log('✅ yt-dlp instalado com sucesso!');
|
|
13
|
-
console.log(`📍 Caminho do binário: ${caminhoBinario}`);
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
const isDirectRun = process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1];
|
|
17
|
-
|
|
18
|
-
if (isDirectRun) {
|
|
19
|
-
instalarYtDlp().catch((erro) => {
|
|
20
|
-
console.error('❌ Erro ao instalar o yt-dlp:');
|
|
21
|
-
console.error(erro.message);
|
|
22
|
-
});
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export { instalarYtDlp };
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import os from 'node:os';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
import { fileURLToPath } from 'node:url';
|
|
5
|
-
import YTDlpWrapImport from 'yt-dlp-wrap';
|
|
6
|
-
|
|
7
|
-
const YTDlpWrap = YTDlpWrapImport?.default || YTDlpWrapImport;
|
|
8
|
-
|
|
9
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
10
|
-
const __dirname = path.dirname(__filename);
|
|
11
|
-
|
|
12
|
-
const DEFAULT_BIN_DIR = path.join(__dirname, 'bin');
|
|
13
|
-
const DEFAULT_BINARY_NAME = os.platform() === 'win32' ? 'yt-dlp.exe' : 'yt-dlp';
|
|
14
|
-
export const DEFAULT_YTDLP_BINARY_PATH = path.join(DEFAULT_BIN_DIR, DEFAULT_BINARY_NAME);
|
|
15
|
-
|
|
16
|
-
export const installYtDlpBinary = async ({ binaryPath = DEFAULT_YTDLP_BINARY_PATH } = {}) => {
|
|
17
|
-
const targetPath = path.resolve(binaryPath);
|
|
18
|
-
const targetDir = path.dirname(targetPath);
|
|
19
|
-
|
|
20
|
-
await fs.promises.mkdir(targetDir, { recursive: true });
|
|
21
|
-
await YTDlpWrap.downloadFromGithub(targetPath, undefined, os.platform());
|
|
22
|
-
|
|
23
|
-
if (os.platform() !== 'win32') {
|
|
24
|
-
await fs.promises.chmod(targetPath, 0o755);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
return targetPath;
|
|
28
|
-
};
|