@kaikybrofc/omnizap-system 2.2.10 → 2.3.1

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.
Files changed (123) hide show
  1. package/README.md +13 -13
  2. package/app/config/adminIdentity.js +1 -3
  3. package/app/connection/socketController.js +10 -20
  4. package/app/controllers/messageController.js +7 -28
  5. package/app/modules/aiModule/catCommand.js +29 -192
  6. package/app/modules/broadcastModule/noticeCommand.js +28 -97
  7. package/app/modules/gameModule/diceCommand.js +6 -32
  8. package/app/modules/playModule/playCommand.js +57 -258
  9. package/app/modules/quoteModule/quoteCommand.js +2 -4
  10. package/app/modules/rpgPokemonModule/rpgPokemonRepository.js +1 -13
  11. package/app/modules/statsModule/noMessageCommand.js +16 -84
  12. package/app/modules/statsModule/rankingCommand.js +5 -25
  13. package/app/modules/statsModule/rankingCommon.js +1 -9
  14. package/app/modules/stickerModule/convertToWebp.js +4 -27
  15. package/app/modules/stickerModule/stickerCommand.js +13 -24
  16. package/app/modules/stickerModule/stickerTextCommand.js +13 -25
  17. package/app/modules/stickerPackModule/autoPackCollectorService.js +16 -7
  18. package/app/modules/stickerPackModule/domainEventOutboxRepository.js +20 -36
  19. package/app/modules/stickerPackModule/domainEvents.js +2 -11
  20. package/app/modules/stickerPackModule/semanticReclassificationEngine.js +13 -50
  21. package/app/modules/stickerPackModule/semanticReclassificationEngine.test.js +2 -15
  22. package/app/modules/stickerPackModule/semanticThemeClusterService.js +14 -41
  23. package/app/modules/stickerPackModule/stickerAssetClassificationRepository.js +25 -95
  24. package/app/modules/stickerPackModule/stickerAssetRepository.js +12 -31
  25. package/app/modules/stickerPackModule/stickerAssetReprocessQueueRepository.js +13 -18
  26. package/app/modules/stickerPackModule/stickerAutoPackByTagsRuntime.js +284 -709
  27. package/app/modules/stickerPackModule/stickerClassificationBackgroundRuntime.js +27 -106
  28. package/app/modules/stickerPackModule/stickerClassificationService.js +46 -77
  29. package/app/modules/stickerPackModule/stickerDedicatedTaskWorkerRuntime.js +13 -53
  30. package/app/modules/stickerPackModule/stickerDomainEventBus.js +10 -16
  31. package/app/modules/stickerPackModule/stickerDomainEventConsumerRuntime.js +40 -39
  32. package/app/modules/stickerPackModule/stickerMarketplaceDriftService.js +1 -4
  33. package/app/modules/stickerPackModule/stickerObjectStorageService.js +26 -26
  34. package/app/modules/stickerPackModule/stickerPackCommandHandlers.js +32 -187
  35. package/app/modules/stickerPackModule/stickerPackInteractionEventRepository.js +6 -15
  36. package/app/modules/stickerPackModule/stickerPackItemRepository.js +6 -32
  37. package/app/modules/stickerPackModule/stickerPackMarketplaceService.js +12 -36
  38. package/app/modules/stickerPackModule/stickerPackMessageService.js +12 -40
  39. package/app/modules/stickerPackModule/stickerPackRepository.js +23 -66
  40. package/app/modules/stickerPackModule/stickerPackScoreSnapshotRepository.js +9 -21
  41. package/app/modules/stickerPackModule/stickerPackScoreSnapshotRuntime.js +10 -40
  42. package/app/modules/stickerPackModule/stickerPackService.js +50 -115
  43. package/app/modules/stickerPackModule/stickerPackServiceRuntime.js +2 -21
  44. package/app/modules/stickerPackModule/stickerPackUtils.js +13 -3
  45. package/app/modules/stickerPackModule/stickerStorageService.js +16 -65
  46. package/app/modules/stickerPackModule/stickerWorkerPipelineRuntime.js +4 -22
  47. package/app/modules/stickerPackModule/stickerWorkerTaskQueueRepository.js +14 -29
  48. package/app/modules/systemMetricsModule/pingCommand.js +9 -39
  49. package/app/modules/tiktokModule/tiktokCommand.js +17 -109
  50. package/app/modules/userModule/userCommand.js +2 -88
  51. package/app/observability/metrics.js +5 -16
  52. package/app/services/captchaService.js +1 -6
  53. package/app/services/dbWriteQueue.js +3 -18
  54. package/app/services/featureFlagService.js +2 -8
  55. package/app/services/newsBroadcastService.js +0 -1
  56. package/app/services/queueUtils.js +2 -4
  57. package/app/services/whatsappLoginLinkService.js +7 -9
  58. package/app/store/premiumUserStore.js +1 -2
  59. package/app/utils/antiLink/antiLinkModule.js +3 -233
  60. package/app/utils/logger/loggerModule.js +9 -34
  61. package/app/utils/systemMetrics/systemMetricsModule.js +1 -4
  62. package/database/index.js +1 -0
  63. package/database/init.js +1 -8
  64. package/database/migrations/20260228_0027_web_visit_event.sql +15 -0
  65. package/docker-compose.yml +27 -27
  66. package/docs/seo/omnizap-seo-playbook-br-2026-02-28.md +26 -0
  67. package/docs/seo/satellite-page-template.md +2 -0
  68. package/docs/seo/satellite-pages-phase1.json +40 -177
  69. package/eslint.config.js +2 -15
  70. package/index.js +8 -36
  71. package/ml/clip_classifier/README.md +4 -6
  72. package/observability/alert-rules.yml +12 -12
  73. package/observability/grafana/provisioning/dashboards/dashboards.yml +1 -1
  74. package/package.json +6 -3
  75. package/public/api-docs/index.html +220 -193
  76. package/public/bot-whatsapp-para-grupo/index.html +291 -261
  77. package/public/bot-whatsapp-sem-programar/index.html +291 -261
  78. package/public/comandos/index.html +421 -406
  79. package/public/como-automatizar-avisos-no-whatsapp/index.html +291 -261
  80. package/public/como-criar-comandos-whatsapp/index.html +291 -261
  81. package/public/como-evitar-spam-no-whatsapp/index.html +291 -261
  82. package/public/como-moderar-grupo-whatsapp/index.html +291 -261
  83. package/public/como-organizar-comunidade-whatsapp/index.html +291 -261
  84. package/public/css/github-project-panel.css +13 -8
  85. package/public/css/stickers-admin.css +25 -9
  86. package/public/css/styles.css +23 -16
  87. package/public/index.html +1106 -993
  88. package/public/js/apps/apiDocsApp.js +17 -167
  89. package/public/js/apps/createPackApp.js +69 -332
  90. package/public/js/apps/homeApp.js +274 -101
  91. package/public/js/apps/loginApp.js +3 -12
  92. package/public/js/apps/stickersAdminApp.js +190 -181
  93. package/public/js/apps/stickersApp.js +482 -1411
  94. package/public/js/apps/userApp.js +217 -1
  95. package/public/js/catalog.js +11 -74
  96. package/public/js/github-panel/components/ErrorState.js +1 -8
  97. package/public/js/github-panel/components/GithubProjectPanel.js +2 -9
  98. package/public/js/github-panel/components/SkeletonPanel.js +1 -11
  99. package/public/js/github-panel/components/StatCard.js +1 -7
  100. package/public/js/github-panel/vendor/react.js +1 -9
  101. package/public/js/runtime/react-runtime.js +1 -9
  102. package/public/licenca/index.html +200 -86
  103. package/public/login/index.html +315 -325
  104. package/public/melhor-bot-whatsapp-para-grupos/index.html +291 -261
  105. package/public/stickers/admin/index.html +14 -19
  106. package/public/stickers/create/index.html +39 -44
  107. package/public/stickers/index.html +96 -107
  108. package/public/termos-de-uso/index.html +369 -122
  109. package/public/user/index.html +527 -350
  110. package/scripts/cache-bust.mjs +5 -24
  111. package/scripts/generate-seo-satellite-pages.mjs +10 -13
  112. package/scripts/run-prettier-all.mjs +25 -0
  113. package/scripts/sticker-catalog-loadtest.mjs +13 -11
  114. package/scripts/sticker-worker-task.mjs +1 -4
  115. package/scripts/sync-readme-snapshot.mjs +3 -2
  116. package/server/auth/googleWebAuth/googleWebAuthService.js +614 -0
  117. package/server/controllers/stickerCatalogController.js +297 -632
  118. package/server/http/httpServer.js +2 -10
  119. package/server/routes/stickerCatalog/catalogHandlers/catalogAdminHttp.js +1 -8
  120. package/server/routes/stickerCatalog/catalogHandlers/catalogAuthHttp.js +1 -9
  121. package/server/routes/stickerCatalog/catalogHandlers/catalogPublicHttp.js +10 -11
  122. package/server/routes/stickerCatalog/catalogHandlers/catalogUploadHttp.js +1 -10
  123. package/server/routes/stickerCatalog/catalogRouter.js +11 -13
@@ -2,7 +2,6 @@ import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import { createHash, randomBytes, randomUUID, scryptSync, timingSafeEqual } from 'node:crypto';
4
4
  import { URL, URLSearchParams } from 'node:url';
5
- import axios from 'axios';
6
5
 
7
6
  import { executeQuery, pool, TABLES } from '../../database/index.js';
8
7
  import { getJidUser, normalizeJid, resolveBotJid } from '../../app/config/baileysConfig.js';
@@ -22,6 +21,7 @@ import { createStickerPackInteractionEvent, listStickerPackInteractionStatsByPac
22
21
  import { buildCreatorRanking, buildIntentCollections, buildPersonalizedRecommendations, buildViewerTagAffinity, computePackSignals } from '../../app/modules/stickerPackModule/stickerPackMarketplaceService.js';
23
22
  import { listStickerPackScoreSnapshotsByPackIds } from '../../app/modules/stickerPackModule/stickerPackScoreSnapshotRepository.js';
24
23
  import { createCatalogApiRouter } from '../routes/stickerCatalog/catalogRouter.js';
24
+ import { createGoogleWebAuthService } from '../auth/googleWebAuth/googleWebAuthService.js';
25
25
  import { buildAdminMenu, buildAiMenu, buildAnimeMenu, buildMediaMenu, buildMenuCaption, buildQuoteMenu, buildStatsMenu, buildStickerMenu } from '../../app/modules/menuModule/common.js';
26
26
  import { getMarketplaceDriftSnapshot } from '../../app/modules/stickerPackModule/stickerMarketplaceDriftService.js';
27
27
  import { getStickerAssetExternalUrl, getStickerStorageConfig, readStickerAssetBuffer, saveStickerAssetFromBuffer } from '../../app/modules/stickerPackModule/stickerStorageService.js';
@@ -100,6 +100,7 @@ const STICKER_CREATE_WEB_PATH = `${STICKER_WEB_PATH}/create`;
100
100
  const STICKER_ADMIN_WEB_PATH = `${STICKER_WEB_PATH}/admin`;
101
101
  const STICKER_LOGIN_WEB_PATH = normalizeBasePath(process.env.STICKER_LOGIN_WEB_PATH, '/login');
102
102
  const USER_PROFILE_WEB_PATH = normalizeBasePath(process.env.USER_PROFILE_WEB_PATH, '/user');
103
+ const STICKER_ADMIN_REDIRECT_TO_USER = parseEnvBool(process.env.STICKER_ADMIN_REDIRECT_TO_USER, true);
103
104
  const STICKER_DATA_PUBLIC_PATH = normalizeBasePath(process.env.STICKER_DATA_PUBLIC_PATH, '/data');
104
105
  const STICKER_DATA_PUBLIC_DIR = path.resolve(process.env.STICKER_DATA_PUBLIC_DIR || path.join(process.cwd(), 'data'));
105
106
  const STICKER_WEB_ASSET_VERSION = sanitizeText(process.env.STICKER_WEB_ASSET_VERSION || '', 64, { allowEmpty: true }) || '';
@@ -189,12 +190,10 @@ const { maxStickerBytes: MAX_STICKER_UPLOAD_BYTES } = getStickerStorageConfig();
189
190
  const MAX_STICKER_SOURCE_UPLOAD_BYTES = Math.max(MAX_STICKER_UPLOAD_BYTES, Number(process.env.STICKER_WEB_UPLOAD_SOURCE_MAX_BYTES) || 20 * 1024 * 1024);
190
191
  const ALLOWED_WEB_UPLOAD_VIDEO_MIMETYPES = new Set(['video/mp4', 'video/webm', 'video/quicktime', 'video/x-m4v']);
191
192
  const webPackEditTokenMap = new Map();
192
- const webGoogleSessionMap = new Map();
193
193
  const adminPanelSessionMap = new Map();
194
- const GOOGLE_WEB_SESSION_COOKIE_NAME = 'omnizap_google_session';
195
194
  const ADMIN_PANEL_SESSION_COOKIE_NAME = 'omnizap_admin_panel_session';
196
- const GOOGLE_WEB_SESSION_DB_TOUCH_INTERVAL_MS = Math.max(30_000, Number(process.env.STICKER_WEB_GOOGLE_SESSION_DB_TOUCH_INTERVAL_MS) || 60_000);
197
- const GOOGLE_WEB_SESSION_DB_PRUNE_INTERVAL_MS = Math.max(5 * 60 * 1000, Number(process.env.STICKER_WEB_GOOGLE_SESSION_DB_PRUNE_INTERVAL_MS) || 60 * 60 * 1000);
195
+ const WEB_VISITOR_COOKIE_NAME = 'omnizap_vid';
196
+ const WEB_SESSION_COOKIE_NAME = 'omnizap_sid';
198
197
  const PACK_WEB_STATUS_VALUES = new Set(['draft', 'uploading', 'processing', 'published', 'failed']);
199
198
  const PACK_WEB_UPLOAD_STATUS_VALUES = new Set(['pending', 'processing', 'done', 'failed']);
200
199
  const WEB_UPLOAD_ERROR_MESSAGE_MAX = 255;
@@ -202,11 +201,12 @@ const WEB_UPLOAD_MAX_CONCURRENCY = Math.max(1, Math.min(6, Number(process.env.ST
202
201
  const WEB_DRAFT_CLEANUP_TTL_MS = Math.max(60 * 60 * 1000, Number(process.env.STICKER_WEB_DRAFT_CLEANUP_TTL_MS) || 24 * 60 * 60 * 1000);
203
202
  const WEB_DRAFT_CLEANUP_RUN_INTERVAL_MS = Math.max(60 * 1000, Number(process.env.STICKER_WEB_DRAFT_CLEANUP_RUN_INTERVAL_MS) || 15 * 60 * 1000);
204
203
  const WEB_UPLOAD_ID_MAX_LENGTH = 120;
205
- let staleDraftCleanupState = {
204
+ const WEB_VISITOR_COOKIE_TTL_SECONDS = clampInt(process.env.WEB_VISITOR_COOKIE_TTL_SECONDS, 60 * 60 * 24 * 365, 60 * 60, 60 * 60 * 24 * 3650);
205
+ const WEB_SESSION_COOKIE_TTL_SECONDS = clampInt(process.env.WEB_SESSION_COOKIE_TTL_SECONDS, 60 * 60 * 24 * 30, 30 * 60, 60 * 60 * 24 * 365);
206
+ const staleDraftCleanupState = {
206
207
  running: false,
207
208
  lastRunAt: 0,
208
209
  };
209
- let googleWebSessionDbPruneAt = 0;
210
210
  let adminPanelSessionPruneAt = 0;
211
211
 
212
212
  const hasPathPrefix = (pathname, prefix) => pathname === prefix || pathname.startsWith(`${prefix}/`);
@@ -870,8 +870,6 @@ const triggerStaleDraftCleanup = () => {
870
870
  maybeCleanupStaleDraftPacks().catch(() => {});
871
871
  };
872
872
 
873
- const GOOGLE_TOKENINFO_URL = 'https://oauth2.googleapis.com/tokeninfo';
874
-
875
873
  const normalizeGoogleSubject = (value) =>
876
874
  String(value || '')
877
875
  .trim()
@@ -884,331 +882,6 @@ const buildGoogleOwnerJid = (googleSub) => {
884
882
  return normalizeJid(`g${normalizedSub}@google.oauth`) || '';
885
883
  };
886
884
 
887
- const verifyGoogleIdToken = async (idToken) => {
888
- const token = String(idToken || '').trim();
889
- if (!token) {
890
- const error = new Error('Token Google ausente.');
891
- error.statusCode = 401;
892
- throw error;
893
- }
894
-
895
- let response;
896
- try {
897
- response = await axios.get(GOOGLE_TOKENINFO_URL, {
898
- params: { id_token: token },
899
- timeout: 5000,
900
- validateStatus: () => true,
901
- });
902
- } catch (error) {
903
- const wrapped = new Error('Falha ao validar login Google.');
904
- wrapped.statusCode = 502;
905
- wrapped.cause = error;
906
- throw wrapped;
907
- }
908
-
909
- if (response.status < 200 || response.status >= 300) {
910
- const reason = String(response?.data?.error_description || response?.data?.error || '').trim();
911
- const error = new Error(reason || 'Token Google inválido.');
912
- error.statusCode = 401;
913
- throw error;
914
- }
915
-
916
- const claims = response?.data && typeof response.data === 'object' ? response.data : {};
917
- const aud = String(claims.aud || '').trim();
918
- const iss = String(claims.iss || '').trim();
919
- const sub = normalizeGoogleSubject(claims.sub);
920
- const email = String(claims.email || '')
921
- .trim()
922
- .toLowerCase();
923
- const emailVerified = String(claims.email_verified || '')
924
- .trim()
925
- .toLowerCase();
926
-
927
- if (STICKER_WEB_GOOGLE_CLIENT_ID && aud !== STICKER_WEB_GOOGLE_CLIENT_ID) {
928
- const error = new Error('Login Google não pertence a este aplicativo.');
929
- error.statusCode = 403;
930
- throw error;
931
- }
932
- if (iss && !['accounts.google.com', 'https://accounts.google.com'].includes(iss)) {
933
- const error = new Error('Emissor do token Google inválido.');
934
- error.statusCode = 401;
935
- throw error;
936
- }
937
- if (!sub) {
938
- const error = new Error('Token Google sem identificador de usuário.');
939
- error.statusCode = 401;
940
- throw error;
941
- }
942
- if (email && emailVerified && !['true', '1'].includes(emailVerified)) {
943
- const error = new Error('Conta Google sem e-mail verificado.');
944
- error.statusCode = 403;
945
- throw error;
946
- }
947
-
948
- return {
949
- sub,
950
- email: email || null,
951
- name: sanitizeText(claims.name || claims.given_name || '', 120, { allowEmpty: true }) || null,
952
- picture: String(claims.picture || '').trim() || null,
953
- };
954
- };
955
-
956
- const pruneExpiredGoogleSessions = () => {
957
- const now = Date.now();
958
- for (const [token, session] of webGoogleSessionMap.entries()) {
959
- if (!session || Number(session.expiresAt || 0) <= now) {
960
- webGoogleSessionMap.delete(token);
961
- }
962
- }
963
- };
964
-
965
- const getGoogleWebSessionTokensFromRequest = (req) => {
966
- const direct = getCookieValuesFromRequest(req, GOOGLE_WEB_SESSION_COOKIE_NAME);
967
- if (direct.length > 0) return direct;
968
- const cookies = parseCookies(req);
969
- const fallback = String(cookies[GOOGLE_WEB_SESSION_COOKIE_NAME] || '').trim();
970
- return fallback ? [fallback] : [];
971
- };
972
-
973
- const getGoogleWebSessionTokenFromRequest = (req) => getGoogleWebSessionTokensFromRequest(req)[0] || '';
974
-
975
- const normalizeGoogleWebSessionRow = (row) => {
976
- if (!row || typeof row !== 'object') return null;
977
- const token = String(row.session_token || '').trim();
978
- const sub = normalizeGoogleSubject(row.google_sub);
979
- const ownerJid = normalizeJid(row.owner_jid) || '';
980
- const ownerPhone = toWhatsAppPhoneDigits(row.owner_phone || ownerJid) || '';
981
- const expiresAt = Number(new Date(row.expires_at || 0));
982
- if (!token || !sub || !ownerJid || !Number.isFinite(expiresAt)) return null;
983
- const createdAtRaw = Number(new Date(row.created_at || 0));
984
- const lastSeenAtRaw = Number(new Date(row.last_seen_at || 0));
985
- return {
986
- token,
987
- sub,
988
- email:
989
- String(row.email || '')
990
- .trim()
991
- .toLowerCase() || null,
992
- name: sanitizeText(row.name || '', 120, { allowEmpty: true }) || null,
993
- picture: String(row.picture_url || '').trim() || null,
994
- ownerJid,
995
- ownerPhone,
996
- createdAt: Number.isFinite(createdAtRaw) ? createdAtRaw : Date.now(),
997
- expiresAt,
998
- lastSeenAt: Number.isFinite(lastSeenAtRaw) ? lastSeenAtRaw : 0,
999
- lastDbTouchAt: Date.now(),
1000
- };
1001
- };
1002
-
1003
- const maybePruneExpiredGoogleSessionsFromDb = async () => {
1004
- const now = Date.now();
1005
- if (now - googleWebSessionDbPruneAt < GOOGLE_WEB_SESSION_DB_PRUNE_INTERVAL_MS) return;
1006
- googleWebSessionDbPruneAt = now;
1007
- try {
1008
- await executeQuery(
1009
- `DELETE FROM ${TABLES.STICKER_WEB_GOOGLE_SESSION}
1010
- WHERE revoked_at IS NOT NULL OR expires_at <= UTC_TIMESTAMP()`,
1011
- );
1012
- } catch (error) {
1013
- logger.warn('Falha ao limpar sessões Google web expiradas do banco.', {
1014
- action: 'sticker_pack_google_web_session_db_prune_failed',
1015
- error: error?.message,
1016
- });
1017
- }
1018
- };
1019
-
1020
- const upsertGoogleWebUserRecord = async (user, connection = null) => {
1021
- const sub = normalizeGoogleSubject(user?.sub);
1022
- const ownerJid = normalizeJid(user?.ownerJid) || '';
1023
- if (!sub || !ownerJid) return;
1024
- const ownerPhone = toWhatsAppPhoneDigits(ownerJid) || null;
1025
- const email =
1026
- String(user?.email || '')
1027
- .trim()
1028
- .toLowerCase() || null;
1029
- const name = sanitizeText(user?.name || '', 120, { allowEmpty: true }) || null;
1030
- const pictureUrl =
1031
- String(user?.picture || '')
1032
- .trim()
1033
- .slice(0, 1024) || null;
1034
-
1035
- // owner_jid e unico; removemos vinculos antigos desse numero antes do upsert para manter 1:1.
1036
- await executeQuery(
1037
- `DELETE FROM ${TABLES.STICKER_WEB_GOOGLE_USER}
1038
- WHERE owner_jid = ?
1039
- AND google_sub <> ?`,
1040
- [ownerJid, sub],
1041
- connection,
1042
- );
1043
-
1044
- await executeQuery(
1045
- `INSERT INTO ${TABLES.STICKER_WEB_GOOGLE_USER}
1046
- (google_sub, owner_jid, email, name, picture_url, last_login_at, last_seen_at)
1047
- VALUES (?, ?, ?, ?, ?, UTC_TIMESTAMP(), UTC_TIMESTAMP())
1048
- ON DUPLICATE KEY UPDATE
1049
- owner_jid = VALUES(owner_jid),
1050
- email = VALUES(email),
1051
- name = VALUES(name),
1052
- picture_url = VALUES(picture_url),
1053
- last_login_at = UTC_TIMESTAMP(),
1054
- last_seen_at = UTC_TIMESTAMP()`,
1055
- [sub, ownerJid, email, name, pictureUrl],
1056
- connection,
1057
- );
1058
-
1059
- // Persistimos o telefone normalizado do owner para facilitar consultas administrativas.
1060
- await executeQuery(
1061
- `UPDATE ${TABLES.STICKER_WEB_GOOGLE_USER}
1062
- SET owner_phone = COALESCE(?, owner_phone)
1063
- WHERE google_sub = ?`,
1064
- [ownerPhone, sub],
1065
- connection,
1066
- ).catch(() => {});
1067
- };
1068
-
1069
- const upsertGoogleWebSessionRecord = async (session, connection = null) => {
1070
- const token = String(session?.token || '').trim();
1071
- const sub = normalizeGoogleSubject(session?.sub);
1072
- const ownerJid = normalizeJid(session?.ownerJid) || '';
1073
- const ownerPhone = toWhatsAppPhoneDigits(session?.ownerPhone || ownerJid) || null;
1074
- const expiresAt = Number(session?.expiresAt || 0);
1075
- if (!token || !sub || !ownerJid || !Number.isFinite(expiresAt) || expiresAt <= 0) return;
1076
- const email =
1077
- String(session?.email || '')
1078
- .trim()
1079
- .toLowerCase() || null;
1080
- const name = sanitizeText(session?.name || '', 120, { allowEmpty: true }) || null;
1081
- const pictureUrl =
1082
- String(session?.picture || '')
1083
- .trim()
1084
- .slice(0, 1024) || null;
1085
-
1086
- await executeQuery(
1087
- `INSERT INTO ${TABLES.STICKER_WEB_GOOGLE_SESSION}
1088
- (session_token, google_sub, owner_jid, email, name, picture_url, expires_at, revoked_at, last_seen_at)
1089
- VALUES (?, ?, ?, ?, ?, ?, ?, NULL, UTC_TIMESTAMP())
1090
- ON DUPLICATE KEY UPDATE
1091
- google_sub = VALUES(google_sub),
1092
- owner_jid = VALUES(owner_jid),
1093
- email = VALUES(email),
1094
- name = VALUES(name),
1095
- picture_url = VALUES(picture_url),
1096
- expires_at = VALUES(expires_at),
1097
- revoked_at = NULL,
1098
- last_seen_at = UTC_TIMESTAMP()`,
1099
- [token, sub, ownerJid, email, name, pictureUrl, new Date(expiresAt)],
1100
- connection,
1101
- );
1102
-
1103
- await executeQuery(
1104
- `UPDATE ${TABLES.STICKER_WEB_GOOGLE_SESSION}
1105
- SET owner_phone = COALESCE(?, owner_phone)
1106
- WHERE session_token = ?`,
1107
- [ownerPhone, token],
1108
- connection,
1109
- ).catch(() => {});
1110
- };
1111
-
1112
- const deleteOtherGoogleWebSessionsInDb = async ({ token = '', ownerJid = '', sub = '' } = {}, connection = null) => {
1113
- const sessionToken = String(token || '').trim();
1114
- const normalizedOwnerJid = normalizeJid(ownerJid) || '';
1115
- const normalizedSub = normalizeGoogleSubject(sub);
1116
- if (!sessionToken || (!normalizedOwnerJid && !normalizedSub)) return 0;
1117
-
1118
- const clauses = [];
1119
- const params = [];
1120
- if (normalizedOwnerJid) {
1121
- clauses.push('owner_jid = ?');
1122
- params.push(normalizedOwnerJid);
1123
- }
1124
- if (normalizedSub) {
1125
- clauses.push('google_sub = ?');
1126
- params.push(normalizedSub);
1127
- }
1128
- if (!clauses.length) return 0;
1129
-
1130
- const result = await executeQuery(
1131
- `DELETE FROM ${TABLES.STICKER_WEB_GOOGLE_SESSION}
1132
- WHERE session_token <> ?
1133
- AND (${clauses.join(' OR ')})`,
1134
- [sessionToken, ...params],
1135
- connection,
1136
- );
1137
- return Number(result?.affectedRows || 0);
1138
- };
1139
-
1140
- const persistGoogleWebSessionToDb = async (session) => {
1141
- if (!session?.token || !session?.sub || !session?.ownerJid) return;
1142
- await maybePruneExpiredGoogleSessionsFromDb();
1143
- await runSqlTransaction(async (connection) => {
1144
- await upsertGoogleWebUserRecord(
1145
- {
1146
- sub: session.sub,
1147
- ownerJid: session.ownerJid,
1148
- email: session.email,
1149
- name: session.name,
1150
- picture: session.picture,
1151
- },
1152
- connection,
1153
- );
1154
- await deleteOtherGoogleWebSessionsInDb(
1155
- {
1156
- token: session.token,
1157
- ownerJid: session.ownerJid,
1158
- sub: session.sub,
1159
- },
1160
- connection,
1161
- );
1162
- await upsertGoogleWebSessionRecord(session, connection);
1163
- });
1164
- };
1165
-
1166
- const findGoogleWebSessionInDbByToken = async (sessionToken) => {
1167
- const token = String(sessionToken || '').trim();
1168
- if (!token) return null;
1169
- await maybePruneExpiredGoogleSessionsFromDb();
1170
- const rows = await executeQuery(
1171
- `SELECT session_token, google_sub, owner_jid, owner_phone, email, name, picture_url, created_at, expires_at, last_seen_at
1172
- FROM ${TABLES.STICKER_WEB_GOOGLE_SESSION}
1173
- WHERE session_token = ?
1174
- AND revoked_at IS NULL
1175
- AND expires_at > UTC_TIMESTAMP()
1176
- LIMIT 1`,
1177
- [token],
1178
- );
1179
- return normalizeGoogleWebSessionRow(Array.isArray(rows) ? rows[0] : null);
1180
- };
1181
-
1182
- const touchGoogleWebSessionSeenInDb = async (sessionToken) => {
1183
- const token = String(sessionToken || '').trim();
1184
- if (!token) return;
1185
- await executeQuery(
1186
- `UPDATE ${TABLES.STICKER_WEB_GOOGLE_SESSION}
1187
- SET last_seen_at = UTC_TIMESTAMP()
1188
- WHERE session_token = ?
1189
- AND revoked_at IS NULL`,
1190
- [token],
1191
- );
1192
- };
1193
-
1194
- const touchGoogleWebUserSeenInDb = async (googleSub) => {
1195
- const sub = normalizeGoogleSubject(googleSub);
1196
- if (!sub) return;
1197
- await executeQuery(
1198
- `UPDATE ${TABLES.STICKER_WEB_GOOGLE_USER}
1199
- SET last_seen_at = UTC_TIMESTAMP()
1200
- WHERE google_sub = ?`,
1201
- [sub],
1202
- );
1203
- };
1204
-
1205
- const deleteGoogleWebSessionFromDb = async (sessionToken) => {
1206
- const token = String(sessionToken || '').trim();
1207
- if (!token) return 0;
1208
- const result = await executeQuery(`DELETE FROM ${TABLES.STICKER_WEB_GOOGLE_SESSION} WHERE session_token = ?`, [token]);
1209
- return Number(result?.affectedRows || 0);
1210
- };
1211
-
1212
885
  const mapAdminBanRow = (row) => {
1213
886
  if (!row || typeof row !== 'object') return null;
1214
887
  return {
@@ -1294,37 +967,11 @@ const createAdminBanRecord = async ({ googleSub = '', email = '', ownerJid = '',
1294
967
  );
1295
968
 
1296
969
  if (normalizedSub || normalizedEmail || normalizedOwnerJid) {
1297
- // Revoke matching Google web sessions immediately.
1298
- const clauses = [];
1299
- const params = [];
1300
- if (normalizedSub) {
1301
- clauses.push('google_sub = ?');
1302
- params.push(normalizedSub);
1303
- }
1304
- if (normalizedEmail) {
1305
- clauses.push('email = ?');
1306
- params.push(normalizedEmail);
1307
- }
1308
- if (normalizedOwnerJid) {
1309
- clauses.push('owner_jid = ?');
1310
- params.push(normalizedOwnerJid);
1311
- }
1312
- if (clauses.length) {
1313
- await executeQuery(
1314
- `DELETE FROM ${TABLES.STICKER_WEB_GOOGLE_SESSION}
1315
- WHERE ${clauses.join(' OR ')}`,
1316
- params,
1317
- ).catch(() => {});
1318
- for (const [token, session] of webGoogleSessionMap.entries()) {
1319
- if (!session) continue;
1320
- const sessionSub = normalizeGoogleSubject(session.sub);
1321
- const sessionEmail = normalizeEmail(session.email);
1322
- const sessionOwner = normalizeJid(session.ownerJid) || '';
1323
- if ((normalizedSub && sessionSub === normalizedSub) || (normalizedEmail && sessionEmail === normalizedEmail) || (normalizedOwnerJid && sessionOwner === normalizedOwnerJid)) {
1324
- webGoogleSessionMap.delete(token);
1325
- }
1326
- }
1327
- }
970
+ await revokeGoogleWebSessionsByIdentity({
971
+ googleSub: normalizedSub,
972
+ email: normalizedEmail,
973
+ ownerJid: normalizedOwnerJid,
974
+ }).catch(() => {});
1328
975
  }
1329
976
 
1330
977
  const rows = await executeQuery(`SELECT * FROM ${TABLES.STICKER_WEB_ADMIN_BAN} WHERE id = ? LIMIT 1`, [banId]);
@@ -1358,6 +1005,42 @@ const assertGoogleIdentityNotBanned = async ({ sub = '', email = '', ownerJid =
1358
1005
  throw error;
1359
1006
  };
1360
1007
 
1008
+ const googleWebSessionDbTouchIntervalMs = Math.max(30_000, Number(process.env.STICKER_WEB_GOOGLE_SESSION_DB_TOUCH_INTERVAL_MS) || 60_000);
1009
+ const googleWebSessionDbPruneIntervalMs = Math.max(5 * 60 * 1000, Number(process.env.STICKER_WEB_GOOGLE_SESSION_DB_PRUNE_INTERVAL_MS) || 60 * 60 * 1000);
1010
+ const googleWebSessionCookiePath = normalizeBasePath(process.env.STICKER_WEB_GOOGLE_SESSION_COOKIE_PATH, '/');
1011
+ const googleWebLegacyCookiePaths = [STICKER_API_BASE_PATH, `${STICKER_API_BASE_PATH}/auth`, STICKER_WEB_PATH, STICKER_LOGIN_WEB_PATH];
1012
+
1013
+ const googleWebAuth = createGoogleWebAuthService({
1014
+ executeQuery,
1015
+ runSqlTransaction,
1016
+ tables: TABLES,
1017
+ logger,
1018
+ sendJson,
1019
+ readJsonBody,
1020
+ parseCookies,
1021
+ getCookieValuesFromRequest,
1022
+ appendSetCookie,
1023
+ buildCookieString,
1024
+ normalizeGoogleSubject,
1025
+ normalizeEmail,
1026
+ normalizeJid,
1027
+ sanitizeText,
1028
+ toIsoOrNull,
1029
+ toWhatsAppPhoneDigits,
1030
+ resolveWhatsAppOwnerJidFromLoginPayload,
1031
+ buildGoogleOwnerJid,
1032
+ assertGoogleIdentityNotBanned,
1033
+ googleClientId: STICKER_WEB_GOOGLE_CLIENT_ID,
1034
+ sessionTtlMs: STICKER_WEB_GOOGLE_SESSION_TTL_MS,
1035
+ sessionDbTouchIntervalMs: googleWebSessionDbTouchIntervalMs,
1036
+ sessionDbPruneIntervalMs: googleWebSessionDbPruneIntervalMs,
1037
+ notAllowedErrorCode: STICKER_PACK_ERROR_CODES.NOT_ALLOWED,
1038
+ sessionCookiePath: googleWebSessionCookiePath,
1039
+ legacyCookiePaths: googleWebLegacyCookiePaths,
1040
+ });
1041
+
1042
+ const { upsertGoogleWebUserRecord, resolveGoogleWebSessionFromRequest, mapGoogleSessionResponseData, handleGoogleAuthSessionRequest, revokeGoogleWebSessionsByIdentity } = googleWebAuth;
1043
+
1361
1044
  const mapAdminModeratorRow = (row) => {
1362
1045
  if (!row || typeof row !== 'object') return null;
1363
1046
  return {
@@ -1732,124 +1415,6 @@ const requireOwnerAdminPanelSession = (req, res) => {
1732
1415
  return session;
1733
1416
  };
1734
1417
 
1735
- const createGoogleWebSession = (claims, { ownerJid } = {}) => {
1736
- pruneExpiredGoogleSessions();
1737
- const token = randomUUID();
1738
- const now = Date.now();
1739
- const resolvedOwnerJid = normalizeJid(ownerJid) || buildGoogleOwnerJid(claims.sub);
1740
- const resolvedOwnerPhone = toWhatsAppPhoneDigits(resolvedOwnerJid) || '';
1741
- const session = {
1742
- token,
1743
- sub: claims.sub,
1744
- email: claims.email || null,
1745
- name: claims.name || null,
1746
- picture: claims.picture || null,
1747
- ownerJid: resolvedOwnerJid,
1748
- ownerPhone: resolvedOwnerPhone,
1749
- createdAt: now,
1750
- expiresAt: now + STICKER_WEB_GOOGLE_SESSION_TTL_MS,
1751
- lastSeenAt: now,
1752
- lastDbTouchAt: 0,
1753
- };
1754
- return session;
1755
- };
1756
-
1757
- const activateGoogleWebSession = (session) => {
1758
- if (!session?.token) return;
1759
- pruneExpiredGoogleSessions();
1760
- webGoogleSessionMap.set(session.token, session);
1761
- for (const [token, existing] of webGoogleSessionMap.entries()) {
1762
- if (!existing || token === session.token) continue;
1763
- const sameOwner = normalizeJid(existing.ownerJid) === normalizeJid(session.ownerJid);
1764
- const sameSub = normalizeGoogleSubject(existing.sub) === normalizeGoogleSubject(session.sub);
1765
- if (sameOwner || sameSub) {
1766
- webGoogleSessionMap.delete(token);
1767
- }
1768
- }
1769
- };
1770
-
1771
- const resolveGoogleWebSessionFromRequest = async (req) => {
1772
- pruneExpiredGoogleSessions();
1773
- const sessionTokens = getGoogleWebSessionTokensFromRequest(req);
1774
- if (!sessionTokens.length) return null;
1775
-
1776
- for (const sessionToken of sessionTokens) {
1777
- const session = webGoogleSessionMap.get(sessionToken);
1778
- if (!session) continue;
1779
- if (Number(session.expiresAt || 0) <= Date.now()) {
1780
- webGoogleSessionMap.delete(sessionToken);
1781
- continue;
1782
- }
1783
-
1784
- const now = Date.now();
1785
- session.lastSeenAt = now;
1786
- if (now - Number(session.lastDbTouchAt || 0) >= GOOGLE_WEB_SESSION_DB_TOUCH_INTERVAL_MS) {
1787
- session.lastDbTouchAt = now;
1788
- void touchGoogleWebSessionSeenInDb(sessionToken).catch((error) => {
1789
- logger.warn('Falha ao atualizar last_seen da sessão Google web.', {
1790
- action: 'sticker_pack_google_web_session_touch_failed',
1791
- error: error?.message,
1792
- });
1793
- });
1794
- void touchGoogleWebUserSeenInDb(session.sub).catch(() => {});
1795
- }
1796
- try {
1797
- await assertGoogleIdentityNotBanned({
1798
- sub: session.sub,
1799
- email: session.email,
1800
- ownerJid: session.ownerJid,
1801
- });
1802
- return session;
1803
- } catch {
1804
- webGoogleSessionMap.delete(sessionToken);
1805
- void deleteGoogleWebSessionFromDb(sessionToken).catch(() => {});
1806
- }
1807
- }
1808
-
1809
- for (const sessionToken of sessionTokens) {
1810
- try {
1811
- const persistedSession = await findGoogleWebSessionInDbByToken(sessionToken);
1812
- if (!persistedSession) continue;
1813
- try {
1814
- await assertGoogleIdentityNotBanned({
1815
- sub: persistedSession.sub,
1816
- email: persistedSession.email,
1817
- ownerJid: persistedSession.ownerJid,
1818
- });
1819
- } catch {
1820
- await deleteGoogleWebSessionFromDb(sessionToken).catch(() => {});
1821
- continue;
1822
- }
1823
- webGoogleSessionMap.set(sessionToken, persistedSession);
1824
- return persistedSession;
1825
- } catch (error) {
1826
- logger.warn('Falha ao resolver sessão Google web no banco.', {
1827
- action: 'sticker_pack_google_web_session_db_resolve_failed',
1828
- error: error?.message,
1829
- });
1830
- }
1831
- }
1832
-
1833
- return null;
1834
- };
1835
-
1836
- const clearGoogleWebSessionCookie = (req, res) => {
1837
- appendSetCookie(
1838
- res,
1839
- buildCookieString(GOOGLE_WEB_SESSION_COOKIE_NAME, '', req, {
1840
- maxAgeSeconds: 0,
1841
- }),
1842
- );
1843
- // Also clear host-only variant (legacy cookie written without Domain).
1844
- appendSetCookie(
1845
- res,
1846
- buildCookieString(GOOGLE_WEB_SESSION_COOKIE_NAME, '', req, {
1847
- maxAgeSeconds: 0,
1848
- domain: false,
1849
- }),
1850
- );
1851
- };
1852
-
1853
1418
  const sendAsset = (req, res, buffer, mimetype = 'image/webp') => {
1854
1419
  const maxAgeSeconds = Math.max(60 * 60 * 24, ASSET_CACHE_SECONDS);
1855
1420
  const staleWhileRevalidateSeconds = Math.min(60 * 60 * 24 * 7, Math.max(300, maxAgeSeconds));
@@ -2171,7 +1736,7 @@ const toOwnerJid = (value) => {
2171
1736
  return normalizeJid(`${digits}@s.whatsapp.net`) || '';
2172
1737
  };
2173
1738
 
2174
- const resolveWebCreateOwnerJid = async (explicitOwner = '') => {
1739
+ const _resolveWebCreateOwnerJid = async (explicitOwner = '') => {
2175
1740
  const explicit = toOwnerJid(explicitOwner);
2176
1741
  if (explicit) return explicit;
2177
1742
 
@@ -2723,6 +2288,96 @@ const normalizeViewerKey = (raw) =>
2723
2288
  .replace(/[^a-zA-Z0-9._:@-]+/g, '')
2724
2289
  .slice(0, 120);
2725
2290
 
2291
+ const normalizeVisitToken = (raw) =>
2292
+ String(raw || '')
2293
+ .trim()
2294
+ .replace(/[^a-zA-Z0-9_-]+/g, '')
2295
+ .slice(0, 80);
2296
+
2297
+ const normalizeVisitPath = (raw) => {
2298
+ const normalized = String(raw || '')
2299
+ .trim()
2300
+ .replace(/\s+/g, '')
2301
+ .slice(0, 255);
2302
+ if (!normalized) return '/';
2303
+ return normalized.startsWith('/') ? normalized : `/${normalized}`;
2304
+ };
2305
+
2306
+ const normalizeVisitSource = (raw) =>
2307
+ String(raw || '')
2308
+ .trim()
2309
+ .toLowerCase()
2310
+ .replace(/[^a-z0-9._-]+/g, '')
2311
+ .slice(0, 32) || 'web';
2312
+
2313
+ const normalizeVisitReferrer = (raw) =>
2314
+ String(raw || '')
2315
+ .trim()
2316
+ .slice(0, 1024) || null;
2317
+
2318
+ const normalizeVisitUserAgent = (raw) =>
2319
+ String(raw || '')
2320
+ .trim()
2321
+ .slice(0, 512) || null;
2322
+
2323
+ const resolveVisitPathFromReferrer = (req) => {
2324
+ const rawReferrer = String(req?.headers?.referer || req?.headers?.referrer || '').trim();
2325
+ if (!rawReferrer) return '/';
2326
+ try {
2327
+ const parsed = new URL(rawReferrer);
2328
+ const requestHost = toRequestHost(req);
2329
+ if (requestHost && parsed.host && parsed.host.toLowerCase() !== requestHost.toLowerCase()) return '/';
2330
+ return normalizeVisitPath(parsed.pathname || '/');
2331
+ } catch {
2332
+ return '/';
2333
+ }
2334
+ };
2335
+
2336
+ const ensureWebVisitCookies = (req, res) => {
2337
+ const cookies = parseCookies(req);
2338
+ const currentVisitor = normalizeVisitToken(cookies[WEB_VISITOR_COOKIE_NAME]);
2339
+ const currentSession = normalizeVisitToken(cookies[WEB_SESSION_COOKIE_NAME]);
2340
+ const visitorKey = currentVisitor || randomUUID();
2341
+ const sessionKey = currentSession || randomUUID();
2342
+
2343
+ if (!currentVisitor) {
2344
+ appendSetCookie(
2345
+ res,
2346
+ buildCookieString(WEB_VISITOR_COOKIE_NAME, visitorKey, req, {
2347
+ maxAgeSeconds: WEB_VISITOR_COOKIE_TTL_SECONDS,
2348
+ }),
2349
+ );
2350
+ }
2351
+
2352
+ appendSetCookie(
2353
+ res,
2354
+ buildCookieString(WEB_SESSION_COOKIE_NAME, sessionKey, req, {
2355
+ maxAgeSeconds: WEB_SESSION_COOKIE_TTL_SECONDS,
2356
+ }),
2357
+ );
2358
+
2359
+ return { visitorKey, sessionKey };
2360
+ };
2361
+
2362
+ const trackWebVisitMetric = (req, res, { pagePath = '/', source = 'web' } = {}) => {
2363
+ if ((req.method || '').toUpperCase() === 'HEAD') return Promise.resolve(false);
2364
+ const { visitorKey, sessionKey } = ensureWebVisitCookies(req, res);
2365
+ const safePath = normalizeVisitPath(pagePath || resolveVisitPathFromReferrer(req));
2366
+ const safeSource = normalizeVisitSource(source);
2367
+ const safeReferrer = normalizeVisitReferrer(req?.headers?.referer || req?.headers?.referrer || '');
2368
+ const safeUserAgent = normalizeVisitUserAgent(req?.headers?.['user-agent'] || '');
2369
+
2370
+ return executeQuery(
2371
+ `INSERT INTO ${TABLES.WEB_VISIT_EVENT}
2372
+ (visitor_key, session_key, page_path, referrer, user_agent, source)
2373
+ VALUES (?, ?, ?, ?, ?, ?)`,
2374
+ [visitorKey, sessionKey, safePath, safeReferrer, safeUserAgent, safeSource],
2375
+ ).catch((error) => {
2376
+ if (error?.code === 'ER_NO_SUCH_TABLE') return false;
2377
+ throw error;
2378
+ });
2379
+ };
2380
+
2726
2381
  const resolveActorKeysFromRequest = (req, url) => {
2727
2382
  const queryViewer = normalizeViewerKey(url.searchParams.get('viewer_key'));
2728
2383
  const headerViewer = normalizeViewerKey(req.headers['x-viewer-key']);
@@ -3033,11 +2688,7 @@ const renderCatalogHtml = async ({ initialPackKey }) => {
3033
2688
 
3034
2689
  const renderPackSeoHtml = ({ packSummary }) => {
3035
2690
  const packName = truncateText(packSummary?.name || packSummary?.pack_key || 'Pack', 95);
3036
- const packDescription = truncateText(
3037
- packSummary?.description
3038
- || `Pack de stickers "${packName}" disponível no catálogo OmniZap para uso em bots e automações WhatsApp via API.`,
3039
- 180,
3040
- );
2691
+ const packDescription = truncateText(packSummary?.description || `Pack de stickers "${packName}" disponível no catálogo OmniZap para uso em bots e automações WhatsApp via API.`, 180);
3041
2692
  const canonicalUrl = toSiteAbsoluteUrl(buildPackWebUrl(packSummary?.pack_key || ''));
3042
2693
  const catalogUrl = toSiteAbsoluteUrl(`${STICKER_WEB_PATH}/`);
3043
2694
  const homeUrl = toSiteAbsoluteUrl('/');
@@ -3648,6 +3299,90 @@ const handleMarketplaceStatsRequest = async (req, res, url) => {
3648
3299
  }
3649
3300
  };
3650
3301
 
3302
+ const handleHomeBootstrapRequest = async (req, res, url) => {
3303
+ const visibility = normalizeCatalogVisibility(url?.searchParams?.get('visibility'));
3304
+ const fetchTimeoutMs = {
3305
+ support: 450,
3306
+ bot_contact: 320,
3307
+ session: 450,
3308
+ stats: 700,
3309
+ system_summary: 700,
3310
+ };
3311
+ const errors = [];
3312
+
3313
+ const visitPath = resolveVisitPathFromReferrer(req);
3314
+ void trackWebVisitMetric(req, res, { pagePath: visitPath, source: 'home_bootstrap' }).catch((error) => {
3315
+ logger.warn('Falha ao registrar visita da home bootstrap.', {
3316
+ action: 'web_visit_track_home_bootstrap_failed',
3317
+ error: error?.message,
3318
+ page_path: visitPath,
3319
+ });
3320
+ });
3321
+
3322
+ const [supportResult, botContactResult, sessionResult, statsResult, systemSummaryResult] = await Promise.allSettled([withTimeout(buildSupportInfo(), fetchTimeoutMs.support), withTimeout(Promise.resolve(buildBotContactInfo()), fetchTimeoutMs.bot_contact), STICKER_WEB_GOOGLE_CLIENT_ID ? withTimeout(resolveGoogleWebSessionFromRequest(req), fetchTimeoutMs.session) : Promise.resolve(null), withTimeout(getMarketplaceStatsCached(visibility), fetchTimeoutMs.stats), withTimeout(getSystemSummaryCached(), fetchTimeoutMs.system_summary)]);
3323
+
3324
+ const support = supportResult.status === 'fulfilled' ? supportResult.value || null : null;
3325
+ if (supportResult.status !== 'fulfilled') {
3326
+ errors.push({
3327
+ source: 'support',
3328
+ message: supportResult.reason?.message || 'support_unavailable',
3329
+ });
3330
+ }
3331
+
3332
+ const botContact = botContactResult.status === 'fulfilled' ? botContactResult.value || null : null;
3333
+ if (botContactResult.status !== 'fulfilled') {
3334
+ errors.push({
3335
+ source: 'bot_contact',
3336
+ message: botContactResult.reason?.message || 'bot_contact_unavailable',
3337
+ });
3338
+ }
3339
+
3340
+ const session = sessionResult.status === 'fulfilled' ? sessionResult.value || null : null;
3341
+ if (sessionResult.status !== 'fulfilled') {
3342
+ errors.push({
3343
+ source: 'session',
3344
+ message: sessionResult.reason?.message || 'session_unavailable',
3345
+ });
3346
+ }
3347
+
3348
+ const statsPayload = statsResult.status === 'fulfilled' ? statsResult.value || null : null;
3349
+ if (statsResult.status !== 'fulfilled') {
3350
+ errors.push({
3351
+ source: 'stats',
3352
+ message: statsResult.reason?.message || 'stats_unavailable',
3353
+ });
3354
+ }
3355
+
3356
+ const systemSummaryPayload = systemSummaryResult.status === 'fulfilled' ? systemSummaryResult.value || null : null;
3357
+ if (systemSummaryResult.status !== 'fulfilled') {
3358
+ errors.push({
3359
+ source: 'system_summary',
3360
+ message: systemSummaryResult.reason?.message || 'system_summary_unavailable',
3361
+ });
3362
+ }
3363
+
3364
+ sendJson(req, res, 200, {
3365
+ data: {
3366
+ support,
3367
+ bot_contact: botContact,
3368
+ session: mapGoogleSessionResponseData(session),
3369
+ stats: statsPayload?.data || null,
3370
+ stats_filters: statsPayload?.filters || null,
3371
+ system_summary: systemSummaryPayload?.data || null,
3372
+ },
3373
+ meta: {
3374
+ visibility,
3375
+ cache_seconds: {
3376
+ stats: HOME_MARKETPLACE_STATS_CACHE_SECONDS,
3377
+ system_summary: SYSTEM_SUMMARY_CACHE_SECONDS,
3378
+ },
3379
+ timeouts_ms: fetchTimeoutMs,
3380
+ partial: errors.length > 0,
3381
+ errors,
3382
+ },
3383
+ });
3384
+ };
3385
+
3651
3386
  const handleCreatePackConfigRequest = async (req, res) => {
3652
3387
  triggerStaleDraftCleanup();
3653
3388
  sendJson(req, res, 200, {
@@ -3703,148 +3438,6 @@ const handleCreatePackConfigRequest = async (req, res) => {
3703
3438
  });
3704
3439
  };
3705
3440
 
3706
- const handleGoogleAuthSessionRequest = async (req, res) => {
3707
- if (!STICKER_WEB_GOOGLE_CLIENT_ID) {
3708
- sendJson(req, res, 404, { error: 'Login Google desabilitado.' });
3709
- return;
3710
- }
3711
-
3712
- if (req.method === 'GET' || req.method === 'HEAD') {
3713
- const session = await resolveGoogleWebSessionFromRequest(req);
3714
- sendJson(req, res, 200, {
3715
- data: mapGoogleSessionResponseData(session),
3716
- });
3717
- return;
3718
- }
3719
-
3720
- if (req.method === 'DELETE') {
3721
- const tokens = getGoogleWebSessionTokensFromRequest(req);
3722
- for (const token of tokens) {
3723
- webGoogleSessionMap.delete(token);
3724
- await deleteGoogleWebSessionFromDb(token).catch((error) => {
3725
- logger.warn('Falha ao remover sessão Google web do banco.', {
3726
- action: 'sticker_pack_google_web_session_db_delete_failed',
3727
- error: error?.message,
3728
- });
3729
- });
3730
- }
3731
- clearGoogleWebSessionCookie(req, res);
3732
- sendJson(req, res, 200, { data: { cleared: true } });
3733
- return;
3734
- }
3735
-
3736
- if (req.method !== 'POST') {
3737
- sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
3738
- return;
3739
- }
3740
-
3741
- let payload = {};
3742
- try {
3743
- payload = await readJsonBody(req);
3744
- } catch (error) {
3745
- sendJson(req, res, Number(error?.statusCode || 400), { error: error?.message || 'Body inválido.' });
3746
- return;
3747
- }
3748
-
3749
- try {
3750
- const claims = await verifyGoogleIdToken(payload?.google_id_token || payload?.id_token);
3751
- const linkedOwner = resolveWhatsAppOwnerJidFromLoginPayload(payload);
3752
- if (!linkedOwner.ownerJid) {
3753
- if (!linkedOwner.hasPayload) {
3754
- sendJson(req, res, 400, {
3755
- error: 'Abra esta pagina pelo link enviado no WhatsApp. Envie "iniciar" no bot para gerar o link de login.',
3756
- code: 'WHATSAPP_LOGIN_LINK_REQUIRED',
3757
- reason: 'missing_link',
3758
- });
3759
- return;
3760
- }
3761
-
3762
- const reason = String(linkedOwner.reason || '')
3763
- .trim()
3764
- .toLowerCase();
3765
- const isUnauthorizedAttempt = ['invalid_signature', 'missing_signature'].includes(reason);
3766
- const statusCode = isUnauthorizedAttempt ? 403 : 400;
3767
- const errorMessage = reason === 'expired' ? 'Link de login expirado. Envie "iniciar" novamente no WhatsApp.' : isUnauthorizedAttempt ? 'Tentativa de login sem permissao detectada. Gere um novo link enviando "iniciar" no privado do bot.' : 'Link de login invalido. Envie "iniciar" novamente no WhatsApp.';
3768
-
3769
- logger.warn('Tentativa de login web bloqueada por validacao do link WhatsApp.', {
3770
- action: 'sticker_pack_google_web_login_link_blocked',
3771
- reason: reason || 'unknown',
3772
- remote_ip: req.socket?.remoteAddress || null,
3773
- user_agent: req.headers?.['user-agent'] || null,
3774
- });
3775
-
3776
- sendJson(req, res, statusCode, {
3777
- error: errorMessage,
3778
- code: 'WHATSAPP_LOGIN_LINK_INVALID',
3779
- reason: reason || 'invalid_link',
3780
- });
3781
- return;
3782
- }
3783
- const ownerJid = linkedOwner.ownerJid;
3784
-
3785
- await assertGoogleIdentityNotBanned({
3786
- sub: claims.sub,
3787
- email: claims.email,
3788
- ownerJid,
3789
- });
3790
- const session = createGoogleWebSession(claims, { ownerJid });
3791
- if (!session.ownerJid) {
3792
- sendJson(req, res, 400, { error: 'Nao foi possivel vincular a conta Google.' });
3793
- return;
3794
- }
3795
- try {
3796
- await persistGoogleWebSessionToDb(session);
3797
- activateGoogleWebSession(session);
3798
- } catch (persistError) {
3799
- logger.error('Falha ao persistir sessão Google web no banco.', {
3800
- action: 'sticker_pack_google_web_session_db_persist_failed',
3801
- error: persistError?.message,
3802
- });
3803
- sendJson(req, res, 500, { error: 'Falha ao salvar sessão Google. Tente novamente.' });
3804
- return;
3805
- }
3806
-
3807
- appendSetCookie(
3808
- res,
3809
- buildCookieString(GOOGLE_WEB_SESSION_COOKIE_NAME, session.token, req, {
3810
- maxAgeSeconds: Math.floor(STICKER_WEB_GOOGLE_SESSION_TTL_MS / 1000),
3811
- }),
3812
- );
3813
- sendJson(req, res, 200, {
3814
- data: mapGoogleSessionResponseData(session),
3815
- });
3816
- } catch (error) {
3817
- sendJson(req, res, Number(error?.statusCode || 401), {
3818
- error: error?.message || 'Login Google inválido.',
3819
- code: STICKER_PACK_ERROR_CODES.NOT_ALLOWED,
3820
- });
3821
- }
3822
- };
3823
-
3824
- const mapGoogleSessionResponseData = (session) =>
3825
- session
3826
- ? {
3827
- authenticated: true,
3828
- provider: 'google',
3829
- user: {
3830
- sub: session.sub,
3831
- email: session.email,
3832
- name: session.name,
3833
- picture: session.picture,
3834
- },
3835
- owner_jid: session.ownerJid,
3836
- owner_phone: toWhatsAppPhoneDigits(session.ownerPhone || session.ownerJid) || null,
3837
- expires_at: toIsoOrNull(session.expiresAt),
3838
- }
3839
- : {
3840
- authenticated: false,
3841
- provider: 'google',
3842
- user: null,
3843
- owner_jid: null,
3844
- owner_phone: null,
3845
- expires_at: null,
3846
- };
3847
-
3848
3441
  const buildOwnerLookupJids = (value) => {
3849
3442
  const normalized = normalizeJid(value) || '';
3850
3443
  if (!normalized || !normalized.includes('@')) return [];
@@ -7138,6 +6731,52 @@ const listAdminKnownGoogleUsers = async ({ limit = 200 } = {}) => {
7138
6731
  }));
7139
6732
  };
7140
6733
 
6734
+ const getWebVisitSummary = async ({ rangeDays = 7, topPathsLimit = 10 } = {}) => {
6735
+ const safeRangeDays = Math.max(1, Math.min(90, Number(rangeDays || 7)));
6736
+ const safeTopPathsLimit = Math.max(1, Math.min(30, Number(topPathsLimit || 10)));
6737
+
6738
+ try {
6739
+ const [countersRows, topPathsRows] = await Promise.all([
6740
+ executeQuery(
6741
+ `SELECT
6742
+ COUNT(*) AS total_events,
6743
+ SUM(CASE WHEN created_at >= (UTC_TIMESTAMP() - INTERVAL 1 DAY) THEN 1 ELSE 0 END) AS events_24h,
6744
+ SUM(CASE WHEN created_at >= (UTC_TIMESTAMP() - INTERVAL ${safeRangeDays} DAY) THEN 1 ELSE 0 END) AS events_range,
6745
+ COUNT(DISTINCT CASE WHEN created_at >= (UTC_TIMESTAMP() - INTERVAL ${safeRangeDays} DAY) THEN visitor_key END) AS unique_visitors_range,
6746
+ COUNT(DISTINCT CASE WHEN created_at >= (UTC_TIMESTAMP() - INTERVAL ${safeRangeDays} DAY) THEN session_key END) AS unique_sessions_range
6747
+ FROM ${TABLES.WEB_VISIT_EVENT}`,
6748
+ ),
6749
+ executeQuery(
6750
+ `SELECT page_path, COUNT(*) AS total
6751
+ FROM ${TABLES.WEB_VISIT_EVENT}
6752
+ WHERE created_at >= (UTC_TIMESTAMP() - INTERVAL ${safeRangeDays} DAY)
6753
+ GROUP BY page_path
6754
+ ORDER BY total DESC
6755
+ LIMIT ${safeTopPathsLimit}`,
6756
+ ),
6757
+ ]);
6758
+
6759
+ const counters = Array.isArray(countersRows) ? countersRows[0] || {} : {};
6760
+ const topPaths = (Array.isArray(topPathsRows) ? topPathsRows : []).map((row) => ({
6761
+ page_path: normalizeVisitPath(row?.page_path || '/'),
6762
+ total: Number(row?.total || 0),
6763
+ }));
6764
+
6765
+ return {
6766
+ range_days: safeRangeDays,
6767
+ total_events: Number(counters?.total_events || 0),
6768
+ events_24h: Number(counters?.events_24h || 0),
6769
+ events_range: Number(counters?.events_range || 0),
6770
+ unique_visitors_range: Number(counters?.unique_visitors_range || 0),
6771
+ unique_sessions_range: Number(counters?.unique_sessions_range || 0),
6772
+ top_paths: topPaths,
6773
+ };
6774
+ } catch (error) {
6775
+ if (error?.code === 'ER_NO_SUCH_TABLE') return null;
6776
+ throw error;
6777
+ }
6778
+ };
6779
+
7141
6780
  const listAdminPacks = async (url) => {
7142
6781
  const q = sanitizeText(url?.searchParams?.get('q') || '', 120, { allowEmpty: true }) || '';
7143
6782
  const owner = normalizeJid(url?.searchParams?.get('owner_jid') || '') || '';
@@ -7312,7 +6951,7 @@ const handleAdminOverviewRequest = async (req, res) => {
7312
6951
  return;
7313
6952
  }
7314
6953
 
7315
- const [marketplaceStats, activeSessions, knownUsers, bans, packsCountRows, stickersCountRows, recentPacks] = await Promise.all([getMarketplaceGlobalStatsCached().catch(() => null), listAdminActiveGoogleWebSessions({ limit: 50 }), listAdminKnownGoogleUsers({ limit: 50 }), listAdminBans({ activeOnly: true, limit: 50 }), executeQuery(`SELECT COUNT(*) AS total FROM ${TABLES.STICKER_PACK} WHERE deleted_at IS NULL`), executeQuery(`SELECT COUNT(*) AS total FROM ${TABLES.STICKER_ASSET}`), listAdminPacks({ searchParams: new URLSearchParams([['limit', '20']]) })]);
6954
+ const [marketplaceStats, activeSessions, knownUsers, bans, packsCountRows, stickersCountRows, recentPacks, visitSummary] = await Promise.all([getMarketplaceGlobalStatsCached().catch(() => null), listAdminActiveGoogleWebSessions({ limit: 50 }), listAdminKnownGoogleUsers({ limit: 50 }), listAdminBans({ activeOnly: true, limit: 50 }), executeQuery(`SELECT COUNT(*) AS total FROM ${TABLES.STICKER_PACK} WHERE deleted_at IS NULL`), executeQuery(`SELECT COUNT(*) AS total FROM ${TABLES.STICKER_ASSET}`), listAdminPacks({ searchParams: new URLSearchParams([['limit', '20']]) }), getWebVisitSummary({ rangeDays: 7, topPathsLimit: 10 }).catch(() => null)]);
7316
6955
 
7317
6956
  sendJson(req, res, 200, {
7318
6957
  data: {
@@ -7324,11 +6963,15 @@ const handleAdminOverviewRequest = async (req, res) => {
7324
6963
  active_google_sessions: Number(activeSessions.length || 0),
7325
6964
  known_google_users: Number(knownUsers.length || 0),
7326
6965
  active_bans: Number(bans.length || 0),
6966
+ visit_events_24h: Number(visitSummary?.events_24h || 0),
6967
+ visit_events_7d: Number(visitSummary?.events_range || 0),
6968
+ unique_visitors_7d: Number(visitSummary?.unique_visitors_range || 0),
7327
6969
  },
7328
6970
  active_sessions: activeSessions,
7329
6971
  users: knownUsers,
7330
6972
  bans,
7331
6973
  recent_packs: recentPacks,
6974
+ visit_metrics: visitSummary,
7332
6975
  },
7333
6976
  });
7334
6977
  };
@@ -7624,6 +7267,7 @@ const catalogApiRouter = createCatalogApiRouter({
7624
7267
  handleCreatorRankingRequest,
7625
7268
  handleRecommendationsRequest,
7626
7269
  handleMarketplaceStatsRequest,
7270
+ handleHomeBootstrapRequest,
7627
7271
  handleCreatePackConfigRequest,
7628
7272
  handleOrphanStickerListRequest,
7629
7273
  handleDataFileListRequest,
@@ -7666,7 +7310,28 @@ const handleCatalogApiRequest = async (req, res, pathname, url) => catalogApiRou
7666
7310
 
7667
7311
  const handleCatalogPageRequest = async (req, res, pathname) => {
7668
7312
  const normalizedPath = pathname.length > 1 ? pathname.replace(/\/+$/, '') : pathname;
7313
+ void trackWebVisitMetric(req, res, { pagePath: normalizedPath || STICKER_WEB_PATH, source: 'catalog_page' }).catch((error) => {
7314
+ logger.warn('Falha ao registrar visita de página do catálogo.', {
7315
+ action: 'web_visit_track_catalog_page_failed',
7316
+ error: error?.message,
7317
+ page_path: normalizedPath || STICKER_WEB_PATH,
7318
+ });
7319
+ });
7320
+
7669
7321
  if (normalizedPath === STICKER_ADMIN_WEB_PATH) {
7322
+ if (STICKER_ADMIN_REDIRECT_TO_USER) {
7323
+ const requestUrl = new URL(req.url || `${STICKER_ADMIN_WEB_PATH}/`, SITE_ORIGIN);
7324
+ const userUrl = new URL(`${USER_PROFILE_WEB_PATH}/`, SITE_ORIGIN);
7325
+ for (const [key, value] of requestUrl.searchParams.entries()) {
7326
+ userUrl.searchParams.append(key, value);
7327
+ }
7328
+ res.statusCode = 302;
7329
+ res.setHeader('Location', `${userUrl.pathname}${userUrl.search}`);
7330
+ res.setHeader('Cache-Control', 'no-store');
7331
+ res.end();
7332
+ return;
7333
+ }
7334
+
7670
7335
  try {
7671
7336
  const html = await renderAdminPanelHtml();
7672
7337
  sendText(req, res, 200, html, 'text/html; charset=utf-8');