@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
|
@@ -3,6 +3,57 @@ import { createRoot } from 'react-dom/client';
|
|
|
3
3
|
import htm from 'htm';
|
|
4
4
|
|
|
5
5
|
const html = htm.bind(React.createElement);
|
|
6
|
+
const rootElement = document.getElementById('terms-react-root');
|
|
7
|
+
|
|
8
|
+
const DEFAULT_SUPPORT_WHATSAPP_NUMBER = '';
|
|
9
|
+
const DEFAULT_SUPPORT_WHATSAPP_URL = '#';
|
|
10
|
+
const DEFAULT_SUPPORT_WHATSAPP_DISPLAY = 'Canal oficial';
|
|
11
|
+
const DEFAULT_SUPPORT_LGPD_TEXT = 'Olá, gostaria de exercer meus direitos de titular de dados (LGPD).';
|
|
12
|
+
|
|
13
|
+
const normalizePhoneDigits = (value) =>
|
|
14
|
+
String(value || '')
|
|
15
|
+
.replace(/\D+/g, '')
|
|
16
|
+
.slice(0, 15);
|
|
17
|
+
|
|
18
|
+
const formatWhatsappDisplay = (value) => {
|
|
19
|
+
const digits = normalizePhoneDigits(value);
|
|
20
|
+
if (!digits) return DEFAULT_SUPPORT_WHATSAPP_DISPLAY;
|
|
21
|
+
if (digits.startsWith('55') && digits.length === 12) {
|
|
22
|
+
return `+55 ${digits.slice(2, 4)} ${digits.slice(4, 8)}-${digits.slice(8)}`;
|
|
23
|
+
}
|
|
24
|
+
if (digits.startsWith('55') && digits.length === 13) {
|
|
25
|
+
return `+55 ${digits.slice(2, 4)} ${digits.slice(4, 9)}-${digits.slice(9)}`;
|
|
26
|
+
}
|
|
27
|
+
return `+${digits}`;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const buildWhatsappUrl = (value, text = '') => {
|
|
31
|
+
const digits = normalizePhoneDigits(value);
|
|
32
|
+
if (!digits) return '';
|
|
33
|
+
const normalizedText = String(text || '').trim();
|
|
34
|
+
if (!normalizedText) return `https://wa.me/${digits}`;
|
|
35
|
+
return `https://wa.me/${digits}?text=${encodeURIComponent(normalizedText)}`;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const isValidWhatsappUrl = (value) => /^https?:\/\/wa\.me\/\d{8,15}(?:\?.*)?$/i.test(String(value || '').trim());
|
|
39
|
+
|
|
40
|
+
const resolveSupportLinks = (root) => {
|
|
41
|
+
const dataset = root?.dataset || {};
|
|
42
|
+
const supportNumber = normalizePhoneDigits(dataset.whatsappSupportNumber) || DEFAULT_SUPPORT_WHATSAPP_NUMBER;
|
|
43
|
+
const rawSupportUrl = String(dataset.whatsappSupportUrl || '').trim();
|
|
44
|
+
const rawSupportLgpdUrl = String(dataset.whatsappSupportLgpdUrl || '').trim();
|
|
45
|
+
const rawSupportDisplay = String(dataset.whatsappSupportDisplay || '').trim();
|
|
46
|
+
const supportUrl = (isValidWhatsappUrl(rawSupportUrl) ? rawSupportUrl : buildWhatsappUrl(supportNumber)) || DEFAULT_SUPPORT_WHATSAPP_URL;
|
|
47
|
+
const supportLgpdUrl = (isValidWhatsappUrl(rawSupportLgpdUrl) ? rawSupportLgpdUrl : buildWhatsappUrl(supportNumber, DEFAULT_SUPPORT_LGPD_TEXT)) || supportUrl;
|
|
48
|
+
const supportDisplay = rawSupportDisplay && !/__WHATSAPP_(?:PUBLIC_CONTACT|SUPPORT)_/i.test(rawSupportDisplay) ? rawSupportDisplay : formatWhatsappDisplay(supportNumber);
|
|
49
|
+
return {
|
|
50
|
+
supportUrl,
|
|
51
|
+
supportLgpdUrl,
|
|
52
|
+
supportDisplay: supportDisplay || DEFAULT_SUPPORT_WHATSAPP_DISPLAY,
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const { supportUrl: SUPPORT_WHATSAPP_URL, supportLgpdUrl: SUPPORT_WHATSAPP_LGPD_URL, supportDisplay: SUPPORT_WHATSAPP_DISPLAY } = resolveSupportLinks(rootElement);
|
|
6
57
|
|
|
7
58
|
const TERMS_CONTENT_HTML = String.raw`
|
|
8
59
|
<section class="terms-card" style="border-bottom: 4px solid hsla(142, 71%, 45%, 0.3)">
|
|
@@ -10,7 +61,7 @@ const TERMS_CONTENT_HTML = String.raw`
|
|
|
10
61
|
<span class="updated">Última atualização: 07/03/2026</span>
|
|
11
62
|
<p>Este instrumento regula o acesso e uso do site, API, painel e funcionalidades de automação disponibilizadas pelo Omnizap.</p>
|
|
12
63
|
<div class="flex flex-wrap gap-3 mt-6">
|
|
13
|
-
<a class="contact-btn wa" href="
|
|
64
|
+
<a class="contact-btn wa" href="${SUPPORT_WHATSAPP_URL}" target="_blank">WhatsApp Oficial</a>
|
|
14
65
|
<a class="contact-btn ig" href="https://www.instagram.com/kaikybrofc/" target="_blank">Instagram</a>
|
|
15
66
|
</div>
|
|
16
67
|
</section>
|
|
@@ -22,7 +73,7 @@ const TERMS_CONTENT_HTML = String.raw`
|
|
|
22
73
|
<li>Nome empresarial: <strong>59.034.123 KAIKY BRITO RIBEIRO</strong>.</li>
|
|
23
74
|
<li>CNPJ: <strong>59.034.123/0001-96</strong>.</li>
|
|
24
75
|
<li>UF do registro: <strong>RR</strong>.</li>
|
|
25
|
-
<li>Canal de contato principal: <a href="
|
|
76
|
+
<li>Canal de contato principal: <a href="${SUPPORT_WHATSAPP_URL}" target="_blank" class="accent">${SUPPORT_WHATSAPP_URL}</a>.</li>
|
|
26
77
|
</ul>
|
|
27
78
|
</section>
|
|
28
79
|
|
|
@@ -152,7 +203,7 @@ const TERMS_CONTENT_HTML = String.raw`
|
|
|
152
203
|
<p>Quando aplicável, o atendimento observará resposta simplificada imediata e declaração clara/completa em até 15 (quinze) dias, conforme LGPD art. 19.</p>
|
|
153
204
|
<p>Para exercício de direitos e demandas de privacidade:</p>
|
|
154
205
|
<div class="flex flex-wrap gap-3 mt-4">
|
|
155
|
-
<a class="contact-btn wa" href="
|
|
206
|
+
<a class="contact-btn wa" href="${SUPPORT_WHATSAPP_LGPD_URL}" target="_blank">Solicitar via WhatsApp</a>
|
|
156
207
|
<a class="contact-btn ig" href="https://www.instagram.com/kaikybrofc/" target="_blank">Contato no Instagram</a>
|
|
157
208
|
</div>
|
|
158
209
|
</section>
|
|
@@ -253,12 +304,12 @@ const TERMS_CONTENT_HTML = String.raw`
|
|
|
253
304
|
<h2>20. Contato e foro</h2>
|
|
254
305
|
<p>Para questões contratuais, privacidade e proteção de dados, utilize os canais oficiais abaixo.</p>
|
|
255
306
|
<div class="flex flex-wrap gap-3 mt-4">
|
|
256
|
-
<a class="contact-btn wa" href="
|
|
307
|
+
<a class="contact-btn wa" href="${SUPPORT_WHATSAPP_URL}" target="_blank">WhatsApp oficial</a>
|
|
257
308
|
<a class="contact-btn ig" href="https://www.instagram.com/kaikybrofc/" target="_blank">Instagram oficial</a>
|
|
258
309
|
</div>
|
|
259
310
|
<ul class="mt-6">
|
|
260
|
-
<li>WhatsApp oficial: <strong
|
|
261
|
-
<li>Link direto: <a href="
|
|
311
|
+
<li>WhatsApp oficial: <strong>${SUPPORT_WHATSAPP_DISPLAY}</strong>.</li>
|
|
312
|
+
<li>Link direto: <a href="${SUPPORT_WHATSAPP_URL}" target="_blank" class="accent">${SUPPORT_WHATSAPP_URL}</a>.</li>
|
|
262
313
|
<li>Instagram oficial: <a href="https://www.instagram.com/kaikybrofc/" target="_blank" class="accent">https://www.instagram.com/kaikybrofc/</a>.</li>
|
|
263
314
|
<li>Contato complementar para notificações formais: privacidade@omnizap.shop.</li>
|
|
264
315
|
<li>Encarregado (LGPD art. 41): Kaiky Brito Ribeiro, contato pelo canal oficial de privacidade.</li>
|
|
@@ -520,8 +571,6 @@ const TermsReactAppWithEffects = () => {
|
|
|
520
571
|
return html`<${TermsReactApp} />`;
|
|
521
572
|
};
|
|
522
573
|
|
|
523
|
-
const rootElement = document.getElementById('terms-react-root');
|
|
524
|
-
|
|
525
574
|
if (rootElement) {
|
|
526
575
|
const root = createRoot(rootElement);
|
|
527
576
|
root.render(html`<${TermsReactAppWithEffects} />`);
|
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
import React, { useEffect, useMemo, useState } from 'react';
|
|
2
|
+
import { createRoot } from 'react-dom/client';
|
|
3
|
+
import htm from 'htm';
|
|
4
|
+
|
|
5
|
+
const html = htm.bind(React.createElement);
|
|
6
|
+
|
|
7
|
+
const DEFAULT_API_BASE_PATH = '/api';
|
|
8
|
+
const DEFAULT_LOGIN_PATH = '/login';
|
|
9
|
+
const DEFAULT_PANEL_PATH = '/user';
|
|
10
|
+
const DEFAULT_PASSWORD_RESET_WEB_PATH = '/user/password-reset';
|
|
11
|
+
|
|
12
|
+
const PASSWORD_RECOVERY_SESSION_QUERY_KEYS = Object.freeze(['session_token', 'recovery_session_token', 'password_recovery_session', 'session', 'token']);
|
|
13
|
+
|
|
14
|
+
const normalizeRoutePath = (value, fallback) => {
|
|
15
|
+
const raw = String(value || '').trim();
|
|
16
|
+
if (!raw) return fallback;
|
|
17
|
+
if (!raw.startsWith('/')) return fallback;
|
|
18
|
+
if (/^\/\//.test(raw)) return fallback;
|
|
19
|
+
return raw;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const normalizeSessionToken = (value) =>
|
|
23
|
+
String(value || '')
|
|
24
|
+
.trim()
|
|
25
|
+
.slice(0, 4096);
|
|
26
|
+
|
|
27
|
+
const normalizeCode = (value) =>
|
|
28
|
+
String(value || '')
|
|
29
|
+
.replace(/\D+/g, '')
|
|
30
|
+
.slice(0, 6);
|
|
31
|
+
|
|
32
|
+
const readSessionTokenFromLocation = () => {
|
|
33
|
+
const url = new URL(window.location.href);
|
|
34
|
+
for (const key of PASSWORD_RECOVERY_SESSION_QUERY_KEYS) {
|
|
35
|
+
const token = normalizeSessionToken(url.searchParams.get(key));
|
|
36
|
+
if (token) return token;
|
|
37
|
+
}
|
|
38
|
+
return '';
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const persistSessionTokenInUrl = (sessionToken) => {
|
|
42
|
+
const normalizedToken = normalizeSessionToken(sessionToken);
|
|
43
|
+
if (!normalizedToken) return;
|
|
44
|
+
const url = new URL(window.location.href);
|
|
45
|
+
const current = normalizeSessionToken(url.searchParams.get('session_token'));
|
|
46
|
+
if (current === normalizedToken) return;
|
|
47
|
+
url.searchParams.set('session_token', normalizedToken);
|
|
48
|
+
window.history.replaceState(null, '', `${url.pathname}${url.search}`);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const formatDateTime = (value) => {
|
|
52
|
+
const parsedMs = Date.parse(String(value || ''));
|
|
53
|
+
if (!Number.isFinite(parsedMs)) return 'n/d';
|
|
54
|
+
return new Intl.DateTimeFormat('pt-BR', { dateStyle: 'short', timeStyle: 'short' }).format(new Date(parsedMs));
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const buildLoginRedirectPath = (loginPath, nextPath) => {
|
|
58
|
+
const safeLoginPath = normalizeRoutePath(loginPath, DEFAULT_LOGIN_PATH);
|
|
59
|
+
const safeNextPath = normalizeRoutePath(nextPath, DEFAULT_PANEL_PATH);
|
|
60
|
+
const loginUrl = new URL(safeLoginPath, window.location.origin);
|
|
61
|
+
loginUrl.searchParams.set('next', safeNextPath);
|
|
62
|
+
return `${loginUrl.pathname}${loginUrl.search}`;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const resolvePasswordResetConfig = (rootElement) => {
|
|
66
|
+
const apiBasePath = String(rootElement?.dataset?.apiBasePath || DEFAULT_API_BASE_PATH).trim() || DEFAULT_API_BASE_PATH;
|
|
67
|
+
const loginPath = normalizeRoutePath(rootElement?.dataset?.loginPath, DEFAULT_LOGIN_PATH);
|
|
68
|
+
const panelPath = normalizeRoutePath(rootElement?.dataset?.panelPath, DEFAULT_PANEL_PATH);
|
|
69
|
+
const passwordResetWebPath = normalizeRoutePath(rootElement?.dataset?.passwordResetWebPath, DEFAULT_PASSWORD_RESET_WEB_PATH);
|
|
70
|
+
return {
|
|
71
|
+
apiBasePath,
|
|
72
|
+
loginPath,
|
|
73
|
+
panelPath,
|
|
74
|
+
passwordResetWebPath,
|
|
75
|
+
};
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const createPasswordResetApi = (apiBasePath) => {
|
|
79
|
+
const sessionPath = `${apiBasePath}/auth/password/recovery/session`;
|
|
80
|
+
const sessionRequestPath = `${sessionPath}/request`;
|
|
81
|
+
const sessionVerifyPath = `${sessionPath}/verify`;
|
|
82
|
+
|
|
83
|
+
const fetchJson = async (url, init = {}, { sessionToken = '' } = {}) => {
|
|
84
|
+
const headers = {
|
|
85
|
+
...(init?.headers || {}),
|
|
86
|
+
};
|
|
87
|
+
const normalizedSessionToken = normalizeSessionToken(sessionToken);
|
|
88
|
+
if (normalizedSessionToken) {
|
|
89
|
+
headers['x-password-recovery-session'] = normalizedSessionToken;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const response = await fetch(url, {
|
|
93
|
+
credentials: 'include',
|
|
94
|
+
...init,
|
|
95
|
+
headers,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
let payload = null;
|
|
99
|
+
try {
|
|
100
|
+
payload = await response.json();
|
|
101
|
+
} catch {
|
|
102
|
+
payload = null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (!response.ok) {
|
|
106
|
+
const error = new Error(payload?.error || `Falha HTTP ${response.status}`);
|
|
107
|
+
error.statusCode = response.status;
|
|
108
|
+
error.code = payload?.code || null;
|
|
109
|
+
error.details = payload?.details || null;
|
|
110
|
+
throw error;
|
|
111
|
+
}
|
|
112
|
+
return payload || {};
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
createSession: () => fetchJson(sessionPath, { method: 'POST' }),
|
|
117
|
+
getSessionStatus: (sessionToken) => fetchJson(sessionPath, { method: 'GET' }, { sessionToken }),
|
|
118
|
+
requestCode: (sessionToken) =>
|
|
119
|
+
fetchJson(
|
|
120
|
+
sessionRequestPath,
|
|
121
|
+
{
|
|
122
|
+
method: 'POST',
|
|
123
|
+
headers: {
|
|
124
|
+
'Content-Type': 'application/json; charset=utf-8',
|
|
125
|
+
},
|
|
126
|
+
body: JSON.stringify({}),
|
|
127
|
+
},
|
|
128
|
+
{ sessionToken },
|
|
129
|
+
),
|
|
130
|
+
verifyCode: (sessionToken, { code = '', password = '' } = {}) =>
|
|
131
|
+
fetchJson(
|
|
132
|
+
sessionVerifyPath,
|
|
133
|
+
{
|
|
134
|
+
method: 'POST',
|
|
135
|
+
headers: {
|
|
136
|
+
'Content-Type': 'application/json; charset=utf-8',
|
|
137
|
+
},
|
|
138
|
+
body: JSON.stringify({
|
|
139
|
+
code: normalizeCode(code),
|
|
140
|
+
password: String(password || ''),
|
|
141
|
+
}),
|
|
142
|
+
},
|
|
143
|
+
{ sessionToken },
|
|
144
|
+
),
|
|
145
|
+
};
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const PasswordResetApp = ({ config }) => {
|
|
149
|
+
const api = useMemo(() => createPasswordResetApi(config.apiBasePath), [config.apiBasePath]);
|
|
150
|
+
|
|
151
|
+
const [bootstrapAttempt, setBootstrapAttempt] = useState(0);
|
|
152
|
+
const [isBootstrapping, setIsBootstrapping] = useState(true);
|
|
153
|
+
const [requiresLogin, setRequiresLogin] = useState(false);
|
|
154
|
+
const [sessionToken, setSessionToken] = useState('');
|
|
155
|
+
const [maskedEmail, setMaskedEmail] = useState('');
|
|
156
|
+
const [expiresAt, setExpiresAt] = useState('');
|
|
157
|
+
const [errorMessage, setErrorMessage] = useState('');
|
|
158
|
+
const [successMessage, setSuccessMessage] = useState('');
|
|
159
|
+
const [requestBusy, setRequestBusy] = useState(false);
|
|
160
|
+
const [verifyBusy, setVerifyBusy] = useState(false);
|
|
161
|
+
const [code, setCode] = useState('');
|
|
162
|
+
const [password, setPassword] = useState('');
|
|
163
|
+
const [passwordConfirm, setPasswordConfirm] = useState('');
|
|
164
|
+
|
|
165
|
+
useEffect(() => {
|
|
166
|
+
let active = true;
|
|
167
|
+
|
|
168
|
+
const bootstrap = async () => {
|
|
169
|
+
setIsBootstrapping(true);
|
|
170
|
+
setRequiresLogin(false);
|
|
171
|
+
setErrorMessage('');
|
|
172
|
+
setSuccessMessage('');
|
|
173
|
+
|
|
174
|
+
let resolvedSessionToken = readSessionTokenFromLocation();
|
|
175
|
+
if (!resolvedSessionToken) {
|
|
176
|
+
try {
|
|
177
|
+
const createPayload = await api.createSession();
|
|
178
|
+
const sessionData = createPayload?.data || {};
|
|
179
|
+
resolvedSessionToken = normalizeSessionToken(sessionData?.session_token);
|
|
180
|
+
if (!resolvedSessionToken) {
|
|
181
|
+
throw new Error('Sessão de redefinição não foi criada corretamente.');
|
|
182
|
+
}
|
|
183
|
+
if (!active) return;
|
|
184
|
+
persistSessionTokenInUrl(resolvedSessionToken);
|
|
185
|
+
setMaskedEmail(String(sessionData?.masked_email || '').trim());
|
|
186
|
+
setExpiresAt(String(sessionData?.expires_at || '').trim());
|
|
187
|
+
} catch (error) {
|
|
188
|
+
if (!active) return;
|
|
189
|
+
setRequiresLogin(Number(error?.statusCode || 0) === 401);
|
|
190
|
+
setErrorMessage(error?.message || 'Não foi possível iniciar a sessão de redefinição.');
|
|
191
|
+
setIsBootstrapping(false);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
try {
|
|
197
|
+
const statusPayload = await api.getSessionStatus(resolvedSessionToken);
|
|
198
|
+
const statusData = statusPayload?.data || {};
|
|
199
|
+
if (!active) return;
|
|
200
|
+
setSessionToken(resolvedSessionToken);
|
|
201
|
+
setMaskedEmail(String(statusData?.masked_email || '').trim());
|
|
202
|
+
setExpiresAt(String(statusData?.expires_at || '').trim());
|
|
203
|
+
setErrorMessage('');
|
|
204
|
+
} catch (error) {
|
|
205
|
+
if (!active) return;
|
|
206
|
+
setErrorMessage(error?.message || 'Sessão de redefinição inválida ou expirada.');
|
|
207
|
+
setRequiresLogin(Number(error?.statusCode || 0) === 401);
|
|
208
|
+
} finally {
|
|
209
|
+
if (active) {
|
|
210
|
+
setIsBootstrapping(false);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
void bootstrap();
|
|
216
|
+
return () => {
|
|
217
|
+
active = false;
|
|
218
|
+
};
|
|
219
|
+
}, [api, bootstrapAttempt]);
|
|
220
|
+
|
|
221
|
+
const handleSendCode = async () => {
|
|
222
|
+
if (!sessionToken || requestBusy) return;
|
|
223
|
+
setErrorMessage('');
|
|
224
|
+
setSuccessMessage('');
|
|
225
|
+
setRequestBusy(true);
|
|
226
|
+
|
|
227
|
+
try {
|
|
228
|
+
const payload = await api.requestCode(sessionToken);
|
|
229
|
+
const data = payload?.data || {};
|
|
230
|
+
const emailHint = String(data?.masked_email || maskedEmail || '').trim();
|
|
231
|
+
const cooldownActive = Boolean(data?.cooldown_active);
|
|
232
|
+
const expiresInSeconds = Number(data?.expires_in_seconds || 0);
|
|
233
|
+
const expiresInMinutes = Number.isFinite(expiresInSeconds) && expiresInSeconds > 0 ? Math.max(1, Math.ceil(expiresInSeconds / 60)) : null;
|
|
234
|
+
|
|
235
|
+
let message = emailHint ? `Código enviado para ${emailHint}.` : 'Código de verificação enviado.';
|
|
236
|
+
if (cooldownActive) {
|
|
237
|
+
message = 'Já existe um código ativo. Use o código mais recente enviado por e-mail.';
|
|
238
|
+
} else if (expiresInMinutes) {
|
|
239
|
+
message = `${message} Validade aproximada: ${expiresInMinutes} minuto(s).`;
|
|
240
|
+
}
|
|
241
|
+
setSuccessMessage(message);
|
|
242
|
+
} catch (error) {
|
|
243
|
+
setErrorMessage(error?.message || 'Não foi possível enviar o código de verificação.');
|
|
244
|
+
} finally {
|
|
245
|
+
setRequestBusy(false);
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
const handleVerify = async (event) => {
|
|
250
|
+
event.preventDefault();
|
|
251
|
+
if (!sessionToken || verifyBusy) return;
|
|
252
|
+
|
|
253
|
+
const normalizedCode = normalizeCode(code);
|
|
254
|
+
const safePassword = String(password || '');
|
|
255
|
+
const safePasswordConfirm = String(passwordConfirm || '');
|
|
256
|
+
|
|
257
|
+
if (!/^\d{6}$/.test(normalizedCode)) {
|
|
258
|
+
setErrorMessage('Informe o código de 6 dígitos enviado por e-mail.');
|
|
259
|
+
setSuccessMessage('');
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (safePassword.trim().length < 8) {
|
|
264
|
+
setErrorMessage('Use uma senha com pelo menos 8 caracteres.');
|
|
265
|
+
setSuccessMessage('');
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (safePassword !== safePasswordConfirm) {
|
|
270
|
+
setErrorMessage('A confirmação da senha não confere.');
|
|
271
|
+
setSuccessMessage('');
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
setErrorMessage('');
|
|
276
|
+
setSuccessMessage('');
|
|
277
|
+
setVerifyBusy(true);
|
|
278
|
+
|
|
279
|
+
try {
|
|
280
|
+
const payload = await api.verifyCode(sessionToken, {
|
|
281
|
+
code: normalizedCode,
|
|
282
|
+
password: safePassword,
|
|
283
|
+
});
|
|
284
|
+
const data = payload?.data || {};
|
|
285
|
+
const isAuthenticated = Boolean(data?.session?.authenticated);
|
|
286
|
+
setSuccessMessage('Senha atualizada com sucesso. Redirecionando...');
|
|
287
|
+
setPassword('');
|
|
288
|
+
setPasswordConfirm('');
|
|
289
|
+
setCode('');
|
|
290
|
+
|
|
291
|
+
const destination = isAuthenticated ? config.panelPath : buildLoginRedirectPath(config.loginPath, config.panelPath);
|
|
292
|
+
window.setTimeout(() => {
|
|
293
|
+
window.location.assign(destination);
|
|
294
|
+
}, 900);
|
|
295
|
+
} catch (error) {
|
|
296
|
+
setErrorMessage(error?.message || 'Falha ao validar o código.');
|
|
297
|
+
} finally {
|
|
298
|
+
setVerifyBusy(false);
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
const loginRedirectPath = useMemo(() => buildLoginRedirectPath(config.loginPath, config.passwordResetWebPath), [config.loginPath, config.passwordResetWebPath]);
|
|
303
|
+
|
|
304
|
+
return html`
|
|
305
|
+
<div className="min-h-screen bg-base-100 font-sans selection:bg-primary selection:text-primary-content">
|
|
306
|
+
<header className="sticky top-0 z-40 border-b border-base-200 bg-base-100/80 backdrop-blur-xl">
|
|
307
|
+
<div className="container mx-auto px-4">
|
|
308
|
+
<div className="flex h-16 items-center justify-between gap-4">
|
|
309
|
+
<a href="/" className="flex items-center gap-2.5 hover:opacity-80 transition-opacity">
|
|
310
|
+
<img src="/assets/images/brand-logo-128.webp" className="w-8 h-8 rounded-xl shadow-sm" alt="Logo" />
|
|
311
|
+
<span className="text-base sm:text-lg font-black tracking-tight">OmniZap<span className="text-primary">.</span></span>
|
|
312
|
+
</a>
|
|
313
|
+
<a href=${config.panelPath} className="btn btn-ghost btn-sm h-9 min-h-0 rounded-xl border border-base-300 hover:border-primary transition-all px-3">
|
|
314
|
+
<span className="text-[10px] sm:text-xs font-bold uppercase tracking-wider">Voltar ao Painel</span>
|
|
315
|
+
</a>
|
|
316
|
+
</div>
|
|
317
|
+
</div>
|
|
318
|
+
</header>
|
|
319
|
+
|
|
320
|
+
<main className="container mx-auto px-4 py-12 lg:py-20 flex flex-col items-center">
|
|
321
|
+
<div className="w-full max-w-md space-y-8">
|
|
322
|
+
<div className="text-center space-y-4">
|
|
323
|
+
<div className="inline-flex items-center gap-2 px-3 py-1.5 rounded-full bg-primary/10 border border-primary/20 text-primary text-[10px] font-bold uppercase tracking-widest">Segurança da Conta</div>
|
|
324
|
+
<h1 className="text-4xl font-black tracking-tight text-balance">Redefinir <span className="text-transparent bg-clip-text bg-gradient-to-r from-primary to-secondary">Senha</span></h1>
|
|
325
|
+
<p className="text-base-content/60 leading-relaxed">Solicite um código por e-mail e confirme sua nova senha com validação segura.</p>
|
|
326
|
+
</div>
|
|
327
|
+
|
|
328
|
+
<div className="glass-card rounded-[2.5rem] p-8 space-y-6 relative overflow-hidden">
|
|
329
|
+
<div className="absolute top-0 right-0 w-32 h-32 bg-primary/5 rounded-full blur-3xl -mr-16 -mt-16"></div>
|
|
330
|
+
|
|
331
|
+
${isBootstrapping
|
|
332
|
+
? html`
|
|
333
|
+
<div className="relative z-10 py-8 text-center space-y-4">
|
|
334
|
+
<span className="loading loading-ring loading-lg text-primary"></span>
|
|
335
|
+
<p className="text-sm text-base-content/60">Preparando sessão de redefinição...</p>
|
|
336
|
+
</div>
|
|
337
|
+
`
|
|
338
|
+
: html`
|
|
339
|
+
<div className="relative z-10 space-y-6">
|
|
340
|
+
${sessionToken
|
|
341
|
+
? html`
|
|
342
|
+
<div className="rounded-2xl border border-base-300 bg-base-200/50 p-4 space-y-2">
|
|
343
|
+
<p className="text-[10px] font-bold uppercase tracking-widest text-base-content/40">Sessão ativa</p>
|
|
344
|
+
<p className="text-sm text-base-content/80">E-mail de destino: <b>${maskedEmail || 'não informado'}</b></p>
|
|
345
|
+
<p className="text-xs text-base-content/55">Expira em: <b>${formatDateTime(expiresAt)}</b></p>
|
|
346
|
+
</div>
|
|
347
|
+
|
|
348
|
+
<button type="button" className="btn btn-outline btn-primary btn-block rounded-2xl h-12 font-bold" disabled=${requestBusy || verifyBusy} onClick=${handleSendCode}>${requestBusy ? 'Enviando código...' : 'Enviar código por e-mail'}</button>
|
|
349
|
+
|
|
350
|
+
<form className="space-y-4" onSubmit=${handleVerify}>
|
|
351
|
+
<div className="form-control gap-2">
|
|
352
|
+
<label className="label py-0">
|
|
353
|
+
<span className="label-text text-[11px] font-bold uppercase tracking-widest text-base-content/50">Código de verificação</span>
|
|
354
|
+
</label>
|
|
355
|
+
<input type="text" inputmode="numeric" maxlength="6" value=${code} onInput=${(event) => setCode(normalizeCode(event.target.value))} placeholder="000000" className="input input-bordered h-12 rounded-2xl font-mono tracking-[0.3em] text-center text-lg" />
|
|
356
|
+
</div>
|
|
357
|
+
<div className="form-control gap-2">
|
|
358
|
+
<label className="label py-0">
|
|
359
|
+
<span className="label-text text-[11px] font-bold uppercase tracking-widest text-base-content/50">Nova senha</span>
|
|
360
|
+
</label>
|
|
361
|
+
<input type="password" autocomplete="new-password" value=${password} onInput=${(event) => setPassword(String(event.target.value || ''))} placeholder="Pelo menos 8 caracteres" className="input input-bordered h-12 rounded-2xl" />
|
|
362
|
+
</div>
|
|
363
|
+
<div className="form-control gap-2">
|
|
364
|
+
<label className="label py-0">
|
|
365
|
+
<span className="label-text text-[11px] font-bold uppercase tracking-widest text-base-content/50">Confirmar senha</span>
|
|
366
|
+
</label>
|
|
367
|
+
<input type="password" autocomplete="new-password" value=${passwordConfirm} onInput=${(event) => setPasswordConfirm(String(event.target.value || ''))} placeholder="Repita a nova senha" className="input input-bordered h-12 rounded-2xl" />
|
|
368
|
+
</div>
|
|
369
|
+
<button type="submit" className="btn btn-primary btn-block h-12 rounded-2xl font-black uppercase tracking-widest text-xs" disabled=${verifyBusy || requestBusy}>${verifyBusy ? 'Validando...' : 'Confirmar nova senha'}</button>
|
|
370
|
+
</form>
|
|
371
|
+
`
|
|
372
|
+
: html`
|
|
373
|
+
<div className="alert alert-warning rounded-2xl bg-warning/15 border border-warning/30 text-warning-content text-sm">
|
|
374
|
+
<span>Não foi possível carregar uma sessão de redefinição válida.</span>
|
|
375
|
+
</div>
|
|
376
|
+
<button type="button" className="btn btn-outline btn-block rounded-2xl" onClick=${() => setBootstrapAttempt((value) => value + 1)}>Tentar novamente</button>
|
|
377
|
+
`}
|
|
378
|
+
${errorMessage
|
|
379
|
+
? html`
|
|
380
|
+
<div className="alert alert-error rounded-2xl bg-error/20 border-none text-error-content text-sm">
|
|
381
|
+
<span>${errorMessage}</span>
|
|
382
|
+
</div>
|
|
383
|
+
`
|
|
384
|
+
: null}
|
|
385
|
+
${successMessage
|
|
386
|
+
? html`
|
|
387
|
+
<div className="alert alert-success rounded-2xl bg-success/20 border-none text-success-content text-sm">
|
|
388
|
+
<span>${successMessage}</span>
|
|
389
|
+
</div>
|
|
390
|
+
`
|
|
391
|
+
: null}
|
|
392
|
+
${requiresLogin ? html` <a href=${loginRedirectPath} className="btn btn-ghost btn-block rounded-2xl border border-base-300"> Entrar para continuar </a> ` : null}
|
|
393
|
+
</div>
|
|
394
|
+
`}
|
|
395
|
+
</div>
|
|
396
|
+
</div>
|
|
397
|
+
</main>
|
|
398
|
+
</div>
|
|
399
|
+
`;
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
const rootElement = document.getElementById('user-password-reset-root');
|
|
403
|
+
if (rootElement) {
|
|
404
|
+
const config = resolvePasswordResetConfig(rootElement);
|
|
405
|
+
createRoot(rootElement).render(html`<${PasswordResetApp} config=${config} />`);
|
|
406
|
+
}
|