@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.
- package/README.md +13 -13
- package/app/config/adminIdentity.js +1 -3
- package/app/connection/socketController.js +10 -20
- package/app/controllers/messageController.js +7 -28
- package/app/modules/aiModule/catCommand.js +29 -192
- package/app/modules/broadcastModule/noticeCommand.js +28 -97
- package/app/modules/gameModule/diceCommand.js +6 -32
- package/app/modules/playModule/playCommand.js +57 -258
- package/app/modules/quoteModule/quoteCommand.js +2 -4
- package/app/modules/rpgPokemonModule/rpgPokemonRepository.js +1 -13
- package/app/modules/statsModule/noMessageCommand.js +16 -84
- package/app/modules/statsModule/rankingCommand.js +5 -25
- package/app/modules/statsModule/rankingCommon.js +1 -9
- package/app/modules/stickerModule/convertToWebp.js +4 -27
- package/app/modules/stickerModule/stickerCommand.js +13 -24
- package/app/modules/stickerModule/stickerTextCommand.js +13 -25
- package/app/modules/stickerPackModule/autoPackCollectorService.js +16 -7
- package/app/modules/stickerPackModule/domainEventOutboxRepository.js +20 -36
- package/app/modules/stickerPackModule/domainEvents.js +2 -11
- package/app/modules/stickerPackModule/semanticReclassificationEngine.js +13 -50
- package/app/modules/stickerPackModule/semanticReclassificationEngine.test.js +2 -15
- package/app/modules/stickerPackModule/semanticThemeClusterService.js +14 -41
- package/app/modules/stickerPackModule/stickerAssetClassificationRepository.js +25 -95
- package/app/modules/stickerPackModule/stickerAssetRepository.js +12 -31
- package/app/modules/stickerPackModule/stickerAssetReprocessQueueRepository.js +13 -18
- package/app/modules/stickerPackModule/stickerAutoPackByTagsRuntime.js +284 -709
- package/app/modules/stickerPackModule/stickerClassificationBackgroundRuntime.js +27 -106
- package/app/modules/stickerPackModule/stickerClassificationService.js +46 -77
- package/app/modules/stickerPackModule/stickerDedicatedTaskWorkerRuntime.js +13 -53
- package/app/modules/stickerPackModule/stickerDomainEventBus.js +10 -16
- package/app/modules/stickerPackModule/stickerDomainEventConsumerRuntime.js +40 -39
- package/app/modules/stickerPackModule/stickerMarketplaceDriftService.js +1 -4
- package/app/modules/stickerPackModule/stickerObjectStorageService.js +26 -26
- package/app/modules/stickerPackModule/stickerPackCommandHandlers.js +32 -187
- package/app/modules/stickerPackModule/stickerPackInteractionEventRepository.js +6 -15
- package/app/modules/stickerPackModule/stickerPackItemRepository.js +6 -32
- package/app/modules/stickerPackModule/stickerPackMarketplaceService.js +12 -36
- package/app/modules/stickerPackModule/stickerPackMessageService.js +12 -40
- package/app/modules/stickerPackModule/stickerPackRepository.js +23 -66
- package/app/modules/stickerPackModule/stickerPackScoreSnapshotRepository.js +9 -21
- package/app/modules/stickerPackModule/stickerPackScoreSnapshotRuntime.js +10 -40
- package/app/modules/stickerPackModule/stickerPackService.js +50 -115
- package/app/modules/stickerPackModule/stickerPackServiceRuntime.js +2 -21
- package/app/modules/stickerPackModule/stickerPackUtils.js +13 -3
- package/app/modules/stickerPackModule/stickerStorageService.js +16 -65
- package/app/modules/stickerPackModule/stickerWorkerPipelineRuntime.js +4 -22
- package/app/modules/stickerPackModule/stickerWorkerTaskQueueRepository.js +14 -29
- package/app/modules/systemMetricsModule/pingCommand.js +9 -39
- package/app/modules/tiktokModule/tiktokCommand.js +17 -109
- package/app/modules/userModule/userCommand.js +2 -88
- package/app/observability/metrics.js +5 -16
- package/app/services/captchaService.js +1 -6
- package/app/services/dbWriteQueue.js +3 -18
- package/app/services/featureFlagService.js +2 -8
- package/app/services/newsBroadcastService.js +0 -1
- package/app/services/queueUtils.js +2 -4
- package/app/services/whatsappLoginLinkService.js +7 -9
- package/app/store/premiumUserStore.js +1 -2
- package/app/utils/antiLink/antiLinkModule.js +3 -233
- package/app/utils/logger/loggerModule.js +9 -34
- package/app/utils/systemMetrics/systemMetricsModule.js +1 -4
- package/database/index.js +1 -0
- package/database/init.js +1 -8
- package/database/migrations/20260228_0027_web_visit_event.sql +15 -0
- package/docker-compose.yml +27 -27
- package/docs/seo/omnizap-seo-playbook-br-2026-02-28.md +26 -0
- package/docs/seo/satellite-page-template.md +2 -0
- package/docs/seo/satellite-pages-phase1.json +40 -177
- package/eslint.config.js +2 -15
- package/index.js +8 -36
- package/ml/clip_classifier/README.md +4 -6
- package/observability/alert-rules.yml +12 -12
- package/observability/grafana/provisioning/dashboards/dashboards.yml +1 -1
- package/package.json +6 -3
- package/public/api-docs/index.html +220 -193
- package/public/bot-whatsapp-para-grupo/index.html +291 -261
- package/public/bot-whatsapp-sem-programar/index.html +291 -261
- package/public/comandos/index.html +421 -406
- package/public/como-automatizar-avisos-no-whatsapp/index.html +291 -261
- package/public/como-criar-comandos-whatsapp/index.html +291 -261
- package/public/como-evitar-spam-no-whatsapp/index.html +291 -261
- package/public/como-moderar-grupo-whatsapp/index.html +291 -261
- package/public/como-organizar-comunidade-whatsapp/index.html +291 -261
- package/public/css/github-project-panel.css +13 -8
- package/public/css/stickers-admin.css +25 -9
- package/public/css/styles.css +23 -16
- package/public/index.html +1106 -993
- package/public/js/apps/apiDocsApp.js +17 -167
- package/public/js/apps/createPackApp.js +69 -332
- package/public/js/apps/homeApp.js +274 -101
- package/public/js/apps/loginApp.js +3 -12
- package/public/js/apps/stickersAdminApp.js +190 -181
- package/public/js/apps/stickersApp.js +482 -1411
- package/public/js/apps/userApp.js +217 -1
- package/public/js/catalog.js +11 -74
- package/public/js/github-panel/components/ErrorState.js +1 -8
- package/public/js/github-panel/components/GithubProjectPanel.js +2 -9
- package/public/js/github-panel/components/SkeletonPanel.js +1 -11
- package/public/js/github-panel/components/StatCard.js +1 -7
- package/public/js/github-panel/vendor/react.js +1 -9
- package/public/js/runtime/react-runtime.js +1 -9
- package/public/licenca/index.html +200 -86
- package/public/login/index.html +315 -325
- package/public/melhor-bot-whatsapp-para-grupos/index.html +291 -261
- package/public/stickers/admin/index.html +14 -19
- package/public/stickers/create/index.html +39 -44
- package/public/stickers/index.html +96 -107
- package/public/termos-de-uso/index.html +369 -122
- package/public/user/index.html +527 -350
- package/scripts/cache-bust.mjs +5 -24
- package/scripts/generate-seo-satellite-pages.mjs +10 -13
- package/scripts/run-prettier-all.mjs +25 -0
- package/scripts/sticker-catalog-loadtest.mjs +13 -11
- package/scripts/sticker-worker-task.mjs +1 -4
- package/scripts/sync-readme-snapshot.mjs +3 -2
- package/server/auth/googleWebAuth/googleWebAuthService.js +614 -0
- package/server/controllers/stickerCatalogController.js +297 -632
- package/server/http/httpServer.js +2 -10
- package/server/routes/stickerCatalog/catalogHandlers/catalogAdminHttp.js +1 -8
- package/server/routes/stickerCatalog/catalogHandlers/catalogAuthHttp.js +1 -9
- package/server/routes/stickerCatalog/catalogHandlers/catalogPublicHttp.js +10 -11
- package/server/routes/stickerCatalog/catalogHandlers/catalogUploadHttp.js +1 -10
- 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
|
|
197
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
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
|
|
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');
|