@omnizap-system/omnizap 2.5.12
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/.clusterfuzzlite/Dockerfile +10 -0
- package/.env.example +907 -0
- package/.github/codeql/codeql-config.yml +10 -0
- package/.github/dependabot.yml +35 -0
- package/.github/workflows/ci.yml +73 -0
- package/.github/workflows/codeql.yml +106 -0
- package/.github/workflows/db-migration-check.yml +98 -0
- package/.github/workflows/dependency-review.yml +22 -0
- package/.github/workflows/deploy.yml +95 -0
- package/.github/workflows/release.yml +106 -0
- package/.github/workflows/security-attest-provenance.yml +51 -0
- package/.github/workflows/security-gitleaks.yml +34 -0
- package/.github/workflows/security-runner-hardening.yml +31 -0
- package/.github/workflows/security-scorecard.yml +44 -0
- package/.github/workflows/security-zap-baseline.yml +44 -0
- package/.github/workflows/security-zap-full-scan.yml +43 -0
- package/.github/workflows/security-zizmor.yml +36 -0
- package/.github/workflows/wiki-sync.yml +44 -0
- package/.gitleaks.toml +15 -0
- package/.prettierrc +34 -0
- package/CODE_OF_CONDUCT.md +114 -0
- package/LICENSE +56 -0
- package/README.md +110 -0
- package/SECURITY.md +110 -0
- package/app/config/index.js +4 -0
- package/app/configParts/adminIdentity.js +92 -0
- package/app/configParts/baileysConfig.js +1818 -0
- package/app/configParts/groupUtils.js +692 -0
- package/app/configParts/loggerConfig.js +394 -0
- package/app/configParts/messagePersistenceService.js +305 -0
- package/app/connection/baileysCompatibility.test.js +40 -0
- package/app/connection/baileysDbAuthState.js +344 -0
- package/app/connection/socketController.js +2243 -0
- package/app/controllers/messageController.js +7 -0
- package/app/controllers/messagePipeline/commandMiddleware.js +146 -0
- package/app/controllers/messagePipeline/conversationMiddleware.js +183 -0
- package/app/controllers/messagePipeline/messagePipelineMiddlewares.test.js +522 -0
- package/app/controllers/messagePipeline/postProcessingMiddleware.js +41 -0
- package/app/controllers/messagePipeline/preProcessingMiddlewares.js +166 -0
- package/app/controllers/messageProcessingPipeline.js +699 -0
- package/app/modules/adminModule/AGENT.md +4056 -0
- package/app/modules/adminModule/adminAiHelpService.js +56 -0
- package/app/modules/adminModule/adminConfigRuntime.js +177 -0
- package/app/modules/adminModule/commandConfig.json +7122 -0
- package/app/modules/adminModule/groupCommandHandlers.js +1823 -0
- package/app/modules/adminModule/groupCommandHandlers.test.js +350 -0
- package/app/modules/adminModule/groupEventHandlers.js +399 -0
- package/app/modules/aiModule/AGENT.md +547 -0
- package/app/modules/aiModule/aiAiHelpService.js +14 -0
- package/app/modules/aiModule/aiConfigRuntime.js +135 -0
- package/app/modules/aiModule/catCommand.js +967 -0
- package/app/modules/aiModule/commandConfig.json +981 -0
- package/app/modules/analyticsModule/messageAnalysisEventRepository.js +83 -0
- package/app/modules/gameModule/AGENT.md +196 -0
- package/app/modules/gameModule/commandConfig.json +366 -0
- package/app/modules/gameModule/diceCommand.js +42 -0
- package/app/modules/gameModule/gameAiHelpService.js +14 -0
- package/app/modules/gameModule/gameConfigRuntime.js +68 -0
- package/app/modules/menuModule/AGENT.md +205 -0
- package/app/modules/menuModule/commandConfig.json +366 -0
- package/app/modules/menuModule/common.js +316 -0
- package/app/modules/menuModule/menuAiHelpService.js +14 -0
- package/app/modules/menuModule/menuConfigRuntime.js +68 -0
- package/app/modules/menuModule/menus.js +66 -0
- package/app/modules/playModule/AGENT.md +321 -0
- package/app/modules/playModule/commandConfig.json +584 -0
- package/app/modules/playModule/playAiHelpService.js +14 -0
- package/app/modules/playModule/playCommand.js +1417 -0
- package/app/modules/playModule/playConfigRuntime.js +68 -0
- package/app/modules/quoteModule/AGENT.md +199 -0
- package/app/modules/quoteModule/commandConfig.json +366 -0
- package/app/modules/quoteModule/quoteAiHelpService.js +14 -0
- package/app/modules/quoteModule/quoteCommand.js +842 -0
- package/app/modules/quoteModule/quoteConfigRuntime.js +68 -0
- package/app/modules/rpgPokemonModule/AGENT.md +229 -0
- package/app/modules/rpgPokemonModule/commandConfig.json +386 -0
- package/app/modules/rpgPokemonModule/rpgBattleCanvasRenderer.js +795 -0
- package/app/modules/rpgPokemonModule/rpgBattleService.js +2110 -0
- package/app/modules/rpgPokemonModule/rpgBattleService.test.js +770 -0
- package/app/modules/rpgPokemonModule/rpgEvolutionUtils.js +22 -0
- package/app/modules/rpgPokemonModule/rpgPokemonAiHelpService.js +14 -0
- package/app/modules/rpgPokemonModule/rpgPokemonCommand.js +174 -0
- package/app/modules/rpgPokemonModule/rpgPokemonConfigRuntime.js +68 -0
- package/app/modules/rpgPokemonModule/rpgPokemonDomain.js +192 -0
- package/app/modules/rpgPokemonModule/rpgPokemonDomain.test.js +93 -0
- package/app/modules/rpgPokemonModule/rpgPokemonEvolution.test.js +46 -0
- package/app/modules/rpgPokemonModule/rpgPokemonMessages.js +746 -0
- package/app/modules/rpgPokemonModule/rpgPokemonRepository.js +1847 -0
- package/app/modules/rpgPokemonModule/rpgPokemonService.js +6839 -0
- package/app/modules/rpgPokemonModule/rpgProfileCanvasRenderer.js +354 -0
- package/app/modules/statsModule/AGENT.md +320 -0
- package/app/modules/statsModule/commandConfig.json +540 -0
- package/app/modules/statsModule/globalRankingCommand.js +64 -0
- package/app/modules/statsModule/rankingCommand.js +41 -0
- package/app/modules/statsModule/rankingCommon.js +1305 -0
- package/app/modules/statsModule/statsAiHelpService.js +14 -0
- package/app/modules/statsModule/statsConfigRuntime.js +68 -0
- package/app/modules/stickerModule/AGENT.md +692 -0
- package/app/modules/stickerModule/addStickerMetadata.js +239 -0
- package/app/modules/stickerModule/commandConfig.json +1216 -0
- package/app/modules/stickerModule/convertToWebp.js +367 -0
- package/app/modules/stickerModule/stickerAiHelpService.js +14 -0
- package/app/modules/stickerModule/stickerCommand.js +446 -0
- package/app/modules/stickerModule/stickerConfigRuntime.js +68 -0
- package/app/modules/stickerModule/stickerConvertCommand.js +159 -0
- package/app/modules/stickerModule/stickerTextCommand.js +653 -0
- package/app/modules/stickerPackModule/AGENT.md +215 -0
- package/app/modules/stickerPackModule/autoPackCollectorRuntime.js +20 -0
- package/app/modules/stickerPackModule/autoPackCollectorService.js +357 -0
- package/app/modules/stickerPackModule/commandConfig.json +387 -0
- package/app/modules/stickerPackModule/domainEventOutboxRepository.js +227 -0
- package/app/modules/stickerPackModule/domainEvents.js +52 -0
- package/app/modules/stickerPackModule/semanticReclassificationEngine.js +429 -0
- package/app/modules/stickerPackModule/semanticReclassificationEngine.test.js +75 -0
- package/app/modules/stickerPackModule/semanticThemeClusterService.js +544 -0
- package/app/modules/stickerPackModule/stickerAssetClassificationRepository.js +400 -0
- package/app/modules/stickerPackModule/stickerAssetRepository.js +400 -0
- package/app/modules/stickerPackModule/stickerAssetReprocessQueueRepository.js +175 -0
- package/app/modules/stickerPackModule/stickerAutoPackByTagsRuntime.js +3702 -0
- package/app/modules/stickerPackModule/stickerClassificationBackgroundRuntime.js +559 -0
- package/app/modules/stickerPackModule/stickerClassificationService.js +557 -0
- package/app/modules/stickerPackModule/stickerDedicatedTaskWorkerRuntime.js +249 -0
- package/app/modules/stickerPackModule/stickerDomainEventBus.js +65 -0
- package/app/modules/stickerPackModule/stickerDomainEventConsumerRuntime.js +208 -0
- package/app/modules/stickerPackModule/stickerMarketplaceDriftService.js +99 -0
- package/app/modules/stickerPackModule/stickerObjectStorageService.js +285 -0
- package/app/modules/stickerPackModule/stickerPackAiHelpService.js +14 -0
- package/app/modules/stickerPackModule/stickerPackCommandHandlers.js +1148 -0
- package/app/modules/stickerPackModule/stickerPackConfigRuntime.js +68 -0
- package/app/modules/stickerPackModule/stickerPackEngagementRepository.js +152 -0
- package/app/modules/stickerPackModule/stickerPackErrors.js +30 -0
- package/app/modules/stickerPackModule/stickerPackInteractionEventRepository.js +101 -0
- package/app/modules/stickerPackModule/stickerPackItemRepository.js +432 -0
- package/app/modules/stickerPackModule/stickerPackMarketplaceService.js +313 -0
- package/app/modules/stickerPackModule/stickerPackMessageService.js +268 -0
- package/app/modules/stickerPackModule/stickerPackRepository.js +450 -0
- package/app/modules/stickerPackModule/stickerPackScoreSnapshotRepository.js +179 -0
- package/app/modules/stickerPackModule/stickerPackScoreSnapshotRuntime.js +271 -0
- package/app/modules/stickerPackModule/stickerPackService.js +733 -0
- package/app/modules/stickerPackModule/stickerPackServiceRuntime.js +32 -0
- package/app/modules/stickerPackModule/stickerPackUtils.js +107 -0
- package/app/modules/stickerPackModule/stickerStorageService.js +559 -0
- package/app/modules/stickerPackModule/stickerWorkerPipelineRuntime.js +242 -0
- package/app/modules/stickerPackModule/stickerWorkerTaskQueueRepository.js +242 -0
- package/app/modules/systemMetricsModule/AGENT.md +193 -0
- package/app/modules/systemMetricsModule/commandConfig.json +344 -0
- package/app/modules/systemMetricsModule/pingCommand.js +399 -0
- package/app/modules/systemMetricsModule/systemMetricsAiHelpService.js +14 -0
- package/app/modules/systemMetricsModule/systemMetricsConfigRuntime.js +68 -0
- package/app/modules/tiktokModule/AGENT.md +196 -0
- package/app/modules/tiktokModule/commandConfig.json +366 -0
- package/app/modules/tiktokModule/tiktokAiHelpService.js +14 -0
- package/app/modules/tiktokModule/tiktokCommand.js +716 -0
- package/app/modules/tiktokModule/tiktokConfigRuntime.js +68 -0
- package/app/modules/userModule/AGENT.md +200 -0
- package/app/modules/userModule/commandConfig.json +386 -0
- package/app/modules/userModule/userAiHelpService.js +14 -0
- package/app/modules/userModule/userCommand.js +1155 -0
- package/app/modules/userModule/userConfigRuntime.js +68 -0
- package/app/modules/waifuPicsModule/AGENT.md +431 -0
- package/app/modules/waifuPicsModule/commandConfig.json +780 -0
- package/app/modules/waifuPicsModule/waifuPicsAiHelpService.js +14 -0
- package/app/modules/waifuPicsModule/waifuPicsCommand.js +586 -0
- package/app/modules/waifuPicsModule/waifuPicsConfigRuntime.js +68 -0
- package/app/observability/metrics.js +766 -0
- package/app/services/ai/aiHelpResponseCacheRepository.js +280 -0
- package/app/services/ai/aiLearningRepository.js +400 -0
- package/app/services/ai/commandConfigEnrichmentRepository.js +769 -0
- package/app/services/ai/commandConfigEnrichmentService.js +452 -0
- package/app/services/ai/commandConfigValidationService.js +443 -0
- package/app/services/ai/commandToolBuilderService.js +192 -0
- package/app/services/ai/conversationRouterService.js +516 -0
- package/app/services/ai/geminiService.js +115 -0
- package/app/services/ai/geminiService.test.js +87 -0
- package/app/services/ai/globalModuleAiHelpService.js +1412 -0
- package/app/services/ai/globalToolCallingService.js +203 -0
- package/app/services/ai/messageCommandExecutionService.js +391 -0
- package/app/services/ai/moduleAiHelpCoreService.js +1099 -0
- package/app/services/ai/moduleAiHelpWrapperFactory.js +65 -0
- package/app/services/ai/moduleCommandConfigRuntimeService.js +113 -0
- package/app/services/ai/moduleToolExecutorService.js +464 -0
- package/app/services/ai/moduleToolRegistryService.js +178 -0
- package/app/services/ai/toolCandidateSelectorService.js +781 -0
- package/app/services/auth/googleWebLinkService.js +80 -0
- package/app/services/auth/whatsappLoginLinkService.js +230 -0
- package/app/services/external/pokeApiService.js +398 -0
- package/app/services/group/groupMetadataService.js +311 -0
- package/app/services/infra/dbWriteQueue.js +874 -0
- package/app/services/infra/featureFlagService.js +131 -0
- package/app/services/infra/queueUtils.js +55 -0
- package/app/services/messaging/captchaService.js +491 -0
- package/app/services/messaging/messagePersistenceService.js +1 -0
- package/app/services/messaging/newsBroadcastService.js +347 -0
- package/app/services/sticker/stickerFocusService.js +347 -0
- package/app/services/sticker/stickerFocusService.test.js +43 -0
- package/app/store/aiPromptStore.js +38 -0
- package/app/store/conversationSessionStore.js +131 -0
- package/app/store/groupConfigStore.js +58 -0
- package/app/store/premiumUserStore.js +54 -0
- package/app/utils/antiLink/antiLinkModule.js +700 -0
- package/app/utils/http/getImageBufferModule.js +18 -0
- package/app/utils/json/jsonSanitizer.js +113 -0
- package/app/utils/json/jsonSanitizer.test.js +40 -0
- package/app/utils/systemMetrics/systemMetricsModule.js +88 -0
- package/app/workers/aiLearningWorker.js +605 -0
- package/app/workers/commandConfigEnrichmentWorker.js +242 -0
- package/database/index.js +2075 -0
- package/database/init.js +151 -0
- package/database/migrations/.gitkeep +0 -0
- package/database/migrations/20260307_d0_hardening_down.sql +64 -0
- package/database/migrations/20260307_d0_hardening_up.sql +79 -0
- package/database/migrations/20260307_d1_terms_acceptance_down.sql +11 -0
- package/database/migrations/20260307_d1_terms_acceptance_up.sql +37 -0
- package/database/migrations/20260307_d2_auth_hardening_down.sql +75 -0
- package/database/migrations/20260307_d2_auth_hardening_up.sql +100 -0
- package/database/migrations/20260314_d7_canonical_sender_down.sql +53 -0
- package/database/migrations/20260314_d7_canonical_sender_up.sql +114 -0
- package/database/migrations/20260406_d30_security_analytics_down.sql +95 -0
- package/database/migrations/20260406_d30_security_analytics_up.sql +292 -0
- package/database/migrations/20260407_d31_web_google_session_token_hardening_down.sql +2 -0
- package/database/migrations/20260407_d31_web_google_session_token_hardening_up.sql +17 -0
- package/database/migrations/20260408_d32_ai_help_response_cache_down.sql +1 -0
- package/database/migrations/20260408_d32_ai_help_response_cache_up.sql +22 -0
- package/database/migrations/20260409_d33_ai_learning_tables_down.sql +4 -0
- package/database/migrations/20260409_d33_ai_learning_tables_up.sql +52 -0
- package/database/migrations/20260410_d34_command_config_enrichment_down.sql +3 -0
- package/database/migrations/20260410_d34_command_config_enrichment_up.sql +48 -0
- package/database/schema.sql +1186 -0
- package/docker-compose.yml +104 -0
- package/docs/audits/stickerCatalogController-out-of-scope.md +103 -0
- package/docs/audits/stickerCatalogController-symbols.md +58 -0
- package/docs/compliance/acceptable-use-policy-2026-03-07.md +35 -0
- package/docs/compliance/dpa-b2b-standard-2026-03-07.md +80 -0
- package/docs/compliance/monthly-compliance-checklist-2026-03-07.md +88 -0
- package/docs/compliance/notice-and-takedown-policy-2026-03-07.md +34 -0
- package/docs/compliance/privacy-policy-2026-03-07.md +75 -0
- package/docs/compliance/subprocessors-inventory-2026-03-07.md +16 -0
- package/docs/database/production-db-evolution-runbook-2026q1.md +365 -0
- package/docs/security/dsar-lgpd-runbook-2026-03-07.md +86 -0
- package/docs/security/incident-response-lgpd-anpd-runbook-2026-03-07.md +77 -0
- package/docs/security/network-hardening-runbook-2026-03-07.md +137 -0
- package/docs/seo/omnizap-seo-playbook-br-2026-02-28.md +238 -0
- package/docs/seo/satellite-page-template.md +116 -0
- package/docs/seo/satellite-pages-phase1.json +364 -0
- package/docs/wiki/Home.md +120 -0
- package/docs/wiki/pair-extraordinaire-2026-03-08.md +3 -0
- package/docs/wiki/recent-changes-2026-03-08.md +47 -0
- package/ecosystem.prod.config.cjs +135 -0
- package/eslint.config.js +89 -0
- package/index.js +488 -0
- package/ml/clip_classifier/Dockerfile +18 -0
- package/ml/clip_classifier/README.md +118 -0
- package/ml/clip_classifier/adaptive_scoring.py +40 -0
- package/ml/clip_classifier/classifier.py +654 -0
- package/ml/clip_classifier/embedding_store.py +481 -0
- package/ml/clip_classifier/env_loader.py +15 -0
- package/ml/clip_classifier/llm_label_expander.py +144 -0
- package/ml/clip_classifier/main.py +213 -0
- package/ml/clip_classifier/requirements.txt +10 -0
- package/ml/clip_classifier/similarity_engine.py +74 -0
- package/new-logo.png +0 -0
- package/observability/alert-rules.yml +60 -0
- package/observability/grafana/dashboards/omnizap-mysql.json +136 -0
- package/observability/grafana/dashboards/omnizap-overview.json +170 -0
- package/observability/grafana/provisioning/dashboards/dashboards.yml +11 -0
- package/observability/grafana/provisioning/datasources/datasources.yml +15 -0
- package/observability/loki-config.yml +38 -0
- package/observability/mysql-setup.sql +46 -0
- package/observability/prometheus.yml +35 -0
- package/observability/promtail-config.yml +84 -0
- package/observability/sticker-catalog-slo.md +83 -0
- package/observability/sticker-scale-hardening-rollout.md +128 -0
- package/package.json +144 -0
- package/public/apple-touch-icon.png +0 -0
- package/public/assets/css/commands-react.input.css +71 -0
- package/public/assets/css/create-pack-react.input.css +31 -0
- package/public/assets/css/home-react.input.css +106 -0
- package/public/assets/css/login-react.input.css +58 -0
- package/public/assets/css/stickers-react.input.css +18 -0
- package/public/assets/css/terms-react.input.css +115 -0
- package/public/assets/css/user-react.input.css +57 -0
- package/public/assets/images/brand-icon-192.png +0 -0
- package/public/assets/images/brand-logo-128.webp +0 -0
- package/public/assets/images/hero-banner-1280.jpg +0 -0
- package/public/comandos/commands-catalog.json +4517 -0
- package/public/css/api-docs.css +161 -0
- package/public/css/stickers-admin.css +1288 -0
- package/public/css/styles.css +679 -0
- package/public/css/systemadm/admin.css +474 -0
- package/public/css/systemadm/base.css +73 -0
- package/public/css/systemadm/components.css +662 -0
- package/public/css/systemadm/layout.css +229 -0
- package/public/css/systemadm/tokens.css +56 -0
- package/public/favicon-16x16.png +0 -0
- package/public/favicon-32x32.png +0 -0
- package/public/favicon.ico +0 -0
- package/public/js/apps/apiDocsApp.js +235 -0
- package/public/js/apps/commandsReactApp.js +528 -0
- package/public/js/apps/createPackApp.js +1646 -0
- package/public/js/apps/homeReactApp.js +942 -0
- package/public/js/apps/loginReactApp.js +496 -0
- package/public/js/apps/stickersAdminApp.js +1753 -0
- package/public/js/apps/stickersApp.js +3797 -0
- package/public/js/apps/termsReactApp.js +528 -0
- package/public/js/apps/userApp.js +2540 -0
- package/public/js/apps/userProfile/actions.js +66 -0
- package/public/js/apps/userReactApp.js +547 -0
- package/public/js/catalog.js +950 -0
- package/public/pages/api-docs.html +40 -0
- package/public/pages/aup.html +158 -0
- package/public/pages/comandos.html +41 -0
- package/public/pages/dpa.html +227 -0
- package/public/pages/home.html +45 -0
- package/public/pages/licenca.html +182 -0
- package/public/pages/login.html +40 -0
- package/public/pages/notice-and-takedown.html +234 -0
- package/public/pages/politica-de-privacidade.html +251 -0
- package/public/pages/seo-bot-whatsapp-para-grupo.html +350 -0
- package/public/pages/seo-bot-whatsapp-sem-programar.html +350 -0
- package/public/pages/seo-como-automatizar-avisos-no-whatsapp.html +350 -0
- package/public/pages/seo-como-criar-comandos-whatsapp.html +350 -0
- package/public/pages/seo-como-evitar-spam-no-whatsapp.html +350 -0
- package/public/pages/seo-como-moderar-grupo-whatsapp.html +350 -0
- package/public/pages/seo-como-organizar-comunidade-whatsapp.html +350 -0
- package/public/pages/seo-melhor-bot-whatsapp-para-grupos.html +350 -0
- package/public/pages/stickers-admin.html +31 -0
- package/public/pages/stickers-create.html +41 -0
- package/public/pages/stickers.html +45 -0
- package/public/pages/suboperadores.html +237 -0
- package/public/pages/termos-de-uso-texto-integral.html +241 -0
- package/public/pages/termos-de-uso.html +41 -0
- package/public/pages/user-password-reset.html +32 -0
- package/public/pages/user-systemadm.html +508 -0
- package/public/pages/user.html +39 -0
- package/public/robots.txt +9 -0
- package/public/site.webmanifest +24 -0
- package/public/sitemap.xml +98 -0
- package/schemas/command-config.schema.json +582 -0
- package/scripts/baileys-compat-smoke.mjs +12 -0
- package/scripts/cache-bust.mjs +142 -0
- package/scripts/deploy.sh +916 -0
- package/scripts/email-broadcast-terms-update.mjs +170 -0
- package/scripts/enrich-command-discovery-fields.mjs +286 -0
- package/scripts/generate-command-config-schema.mjs +273 -0
- package/scripts/generate-commands-catalog.mjs +308 -0
- package/scripts/generate-module-agents.mjs +631 -0
- package/scripts/generate-seo-satellite-pages.mjs +400 -0
- package/scripts/github-deploy-notify.mjs +174 -0
- package/scripts/github-release-notify.mjs +219 -0
- package/scripts/release.sh +599 -0
- package/scripts/run-codeql-local.sh +116 -0
- package/scripts/run-prettier-all.mjs +25 -0
- package/scripts/security-smoketest.mjs +581 -0
- package/scripts/sticker-catalog-loadtest.mjs +210 -0
- package/scripts/sticker-worker-task.mjs +119 -0
- package/scripts/sync-readme-snapshot.mjs +133 -0
- package/scripts/validate-command-config-schema.mjs +130 -0
- package/scripts/validate-command-configs.mjs +15 -0
- package/scripts/wiki-sync.sh +191 -0
- package/server/auth/googleWebAuth/googleWebAuthRuntime.js +62 -0
- package/server/auth/googleWebAuth/googleWebAuthService.js +807 -0
- package/server/auth/jwt/webJwtService.js +147 -0
- package/server/auth/stickerCatalogAuthContext.js +165 -0
- package/server/auth/termsAcceptance/termsAcceptanceHandler.js +189 -0
- package/server/auth/userPassword/index.js +14 -0
- package/server/auth/userPassword/userPasswordAuthService.js +422 -0
- package/server/auth/userPassword/userPasswordCrypto.js +199 -0
- package/server/auth/userPassword/userPasswordCrypto.test.js +76 -0
- package/server/auth/userPassword/userPasswordRecoveryService.js +728 -0
- package/server/auth/validation/authSchemas.js +236 -0
- package/server/auth/webAccount/webAccountHandlers.js +1434 -0
- package/server/controllers/admin/adminBanService.js +138 -0
- package/server/controllers/admin/adminPanelHandlers.js +2083 -0
- package/server/controllers/admin/stickerCatalogAdminContext.js +17 -0
- package/server/controllers/admin/systemAdminController.js +201 -0
- package/server/controllers/email/emailAutomationController.js +239 -0
- package/server/controllers/metricsController.js +21 -0
- package/server/controllers/seo/stickerCatalogSeoContext.js +514 -0
- package/server/controllers/sticker/nonCatalogHandlers.js +303 -0
- package/server/controllers/sticker/stickerCatalogController.js +4700 -0
- package/server/controllers/system/contactController.js +115 -0
- package/server/controllers/system/githubController.js +137 -0
- package/server/controllers/system/stickerCatalogSystemContext.js +758 -0
- package/server/controllers/system/storageController.js +154 -0
- package/server/controllers/system/systemController.js +135 -0
- package/server/controllers/system/systemMetricsController.js +156 -0
- package/server/controllers/system/visitController.js +90 -0
- package/server/controllers/userController.js +145 -0
- package/server/email/emailAutomationRuntime.js +225 -0
- package/server/email/emailAutomationService.js +125 -0
- package/server/email/emailOutboxRepository.js +282 -0
- package/server/email/emailTemplateService.js +480 -0
- package/server/email/emailTransportService.js +156 -0
- package/server/http/clientIp.js +95 -0
- package/server/http/httpRequestUtils.js +262 -0
- package/server/http/httpRequestUtils.test.js +80 -0
- package/server/http/httpServer.js +180 -0
- package/server/http/requestContext.js +20 -0
- package/server/http/siteRoutingUtils.js +87 -0
- package/server/index.js +1 -0
- package/server/middleware/cachePolicy.js +26 -0
- package/server/middleware/cachePolicyHelpers.js +1 -0
- package/server/middleware/endpointRateLimit.js +181 -0
- package/server/middleware/rateLimit.js +70 -0
- package/server/middleware/requireAdminAuth.js +48 -0
- package/server/middleware/securityHeaders.js +97 -0
- package/server/routes/admin/systemAdminRouter.js +64 -0
- package/server/routes/email/emailAutomationRouter.js +46 -0
- package/server/routes/health/healthRouter.js +41 -0
- package/server/routes/indexRouter.js +234 -0
- package/server/routes/metrics/metricsRouter.js +58 -0
- package/server/routes/static/staticPageRouter.js +134 -0
- package/server/routes/sticker/catalogHandlers/catalogAdminHttp.js +105 -0
- package/server/routes/sticker/catalogHandlers/catalogAuthHttp.js +77 -0
- package/server/routes/sticker/catalogHandlers/catalogPublicHttp.js +120 -0
- package/server/routes/sticker/catalogHandlers/catalogUploadHttp.js +83 -0
- package/server/routes/sticker/catalogRouter.js +77 -0
- package/server/routes/sticker/stickerApiRouter.js +84 -0
- package/server/routes/sticker/stickerDataRouter.js +145 -0
- package/server/routes/sticker/stickerSiteRouter.js +43 -0
- package/server/routes/user/userApiPaths.js +66 -0
- package/server/routes/user/userRouter.js +65 -0
- package/server/utils/safePath.js +26 -0
- package/utils/logger/loggerModule.js +35 -0
- package/vite.config.mjs +38 -0
|
@@ -0,0 +1,1434 @@
|
|
|
1
|
+
import { scryptSync } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
const MY_PROFILE_DEFAULT_STATS = Object.freeze({
|
|
4
|
+
total: 0,
|
|
5
|
+
published: 0,
|
|
6
|
+
drafts: 0,
|
|
7
|
+
private: 0,
|
|
8
|
+
unlisted: 0,
|
|
9
|
+
public: 0,
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
const buildMyProfileStatsTemplate = () => ({ ...MY_PROFILE_DEFAULT_STATS });
|
|
13
|
+
|
|
14
|
+
const normalizeMyProfileView = (value) => {
|
|
15
|
+
const normalized = String(value || '')
|
|
16
|
+
.trim()
|
|
17
|
+
.toLowerCase();
|
|
18
|
+
if (normalized === 'summary') return 'summary';
|
|
19
|
+
if (normalized === 'packs') return 'packs';
|
|
20
|
+
return 'full';
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const toUserPasswordStatePayload = (credential) => {
|
|
24
|
+
if (!credential) {
|
|
25
|
+
return {
|
|
26
|
+
configured: false,
|
|
27
|
+
failed_attempts: 0,
|
|
28
|
+
last_failed_at: null,
|
|
29
|
+
last_login_at: null,
|
|
30
|
+
password_changed_at: null,
|
|
31
|
+
revoked_at: null,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
configured: Boolean(credential.has_password && !credential.revoked_at),
|
|
37
|
+
failed_attempts: Number(credential.failed_attempts || 0),
|
|
38
|
+
last_failed_at: credential.last_failed_at || null,
|
|
39
|
+
last_login_at: credential.last_login_at || null,
|
|
40
|
+
password_changed_at: credential.password_changed_at || null,
|
|
41
|
+
revoked_at: credential.revoked_at || null,
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const maskEmailForResponse = (value, { normalizeEmail }) => {
|
|
46
|
+
const normalized = normalizeEmail(value);
|
|
47
|
+
if (!normalized || !normalized.includes('@')) return null;
|
|
48
|
+
const [localPart, domainPart] = normalized.split('@');
|
|
49
|
+
if (!localPart || !domainPart) return null;
|
|
50
|
+
const safeLocal = localPart.length <= 2 ? `${localPart.charAt(0) || '*'}*` : `${localPart.slice(0, 2)}***`;
|
|
51
|
+
const domainSegments = domainPart.split('.');
|
|
52
|
+
const domainHead = String(domainSegments.shift() || '');
|
|
53
|
+
const safeDomainHead = domainHead.length <= 2 ? `${domainHead.charAt(0) || '*'}*` : `${domainHead.slice(0, 2)}***`;
|
|
54
|
+
const suffix = domainSegments.length ? `.${domainSegments.join('.')}` : '';
|
|
55
|
+
return `${safeLocal}@${safeDomainHead}${suffix}`;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const normalizePasswordRecoverySessionToken = (value) =>
|
|
59
|
+
String(value || '')
|
|
60
|
+
.trim()
|
|
61
|
+
.slice(0, 4096);
|
|
62
|
+
|
|
63
|
+
const PASSWORD_RECOVERY_SESSION_HEADER_KEYS = Object.freeze(['x-password-recovery-session', 'x-recovery-session-token']);
|
|
64
|
+
|
|
65
|
+
const PASSWORD_RECOVERY_SESSION_BODY_KEYS = Object.freeze(['session_token', 'recovery_session_token', 'password_recovery_session']);
|
|
66
|
+
|
|
67
|
+
const PASSWORD_LOGIN_IDENTITY_HASH_NAMESPACE = 'web_user_password_login_identity';
|
|
68
|
+
const PASSWORD_LOGIN_IDENTITY_KDF_SALT = 'web_user_password_login_identity_salt_v1';
|
|
69
|
+
const PASSWORD_LOGIN_IDENTITY_KDF_OPTIONS = Object.freeze({
|
|
70
|
+
N: 1 << 14,
|
|
71
|
+
r: 8,
|
|
72
|
+
p: 1,
|
|
73
|
+
maxmem: 64 * 1024 * 1024,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const hashPasswordLoginIdentityKey = (identityKey) => {
|
|
77
|
+
const normalizedKey = String(identityKey || '').trim();
|
|
78
|
+
if (!normalizedKey) return null;
|
|
79
|
+
return scryptSync(`${PASSWORD_LOGIN_IDENTITY_HASH_NAMESPACE}|${normalizedKey}`, PASSWORD_LOGIN_IDENTITY_KDF_SALT, 32, PASSWORD_LOGIN_IDENTITY_KDF_OPTIONS);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const buildPasswordRecoverySessionPath = ({ userPasswordResetWebPath = '', userProfileWebPath = '' }) => {
|
|
83
|
+
const safeResetPath = String(userPasswordResetWebPath || '').trim();
|
|
84
|
+
if (safeResetPath) return safeResetPath;
|
|
85
|
+
const safeProfilePath = String(userProfileWebPath || '').trim();
|
|
86
|
+
if (!safeProfilePath) return '/';
|
|
87
|
+
return safeProfilePath.endsWith('/') ? safeProfilePath : `${safeProfilePath}/`;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const resolvePasswordRecoverySessionTokenFromRequest = (req, payload = null) => {
|
|
91
|
+
for (const key of PASSWORD_RECOVERY_SESSION_HEADER_KEYS) {
|
|
92
|
+
const token = normalizePasswordRecoverySessionToken(req?.headers?.[key]);
|
|
93
|
+
if (token) return token;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const authHeader = String(req?.headers?.authorization || '').trim();
|
|
97
|
+
if (authHeader) {
|
|
98
|
+
const [scheme = '', rawToken = ''] = authHeader.split(/\s+/, 2);
|
|
99
|
+
if (scheme.toLowerCase() === 'bearer') {
|
|
100
|
+
const normalizedBearerToken = normalizePasswordRecoverySessionToken(rawToken);
|
|
101
|
+
if (normalizedBearerToken) return normalizedBearerToken;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) return '';
|
|
106
|
+
for (const key of PASSWORD_RECOVERY_SESSION_BODY_KEYS) {
|
|
107
|
+
const token = normalizePasswordRecoverySessionToken(payload?.[key]);
|
|
108
|
+
if (token) return token;
|
|
109
|
+
}
|
|
110
|
+
return '';
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const toPasswordRecoverySessionExpiresAt = (claims) => {
|
|
114
|
+
const expUnix = Number(claims?.exp || 0);
|
|
115
|
+
if (!Number.isFinite(expUnix) || expUnix <= 0) return null;
|
|
116
|
+
return new Date(expUnix * 1000).toISOString();
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const toPasswordRecoverySessionExpiresIn = (claims) => {
|
|
120
|
+
const expUnix = Number(claims?.exp || 0);
|
|
121
|
+
if (!Number.isFinite(expUnix) || expUnix <= 0) return null;
|
|
122
|
+
return Math.max(0, Math.floor(expUnix - Date.now() / 1000));
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const signPasswordRecoverySessionToken = ({ sub = '', email = '', ownerJid = '' } = {}, { isWebAuthJwtEnabled, signWebAuthJwt, passwordRecoverySessionAuthMethod, passwordRecoverySessionTtlSeconds }) => {
|
|
126
|
+
if (!isWebAuthJwtEnabled()) return '';
|
|
127
|
+
return signWebAuthJwt(
|
|
128
|
+
{
|
|
129
|
+
sub,
|
|
130
|
+
email,
|
|
131
|
+
ownerJid,
|
|
132
|
+
authMethod: passwordRecoverySessionAuthMethod,
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
expiresInSeconds: passwordRecoverySessionTtlSeconds,
|
|
136
|
+
},
|
|
137
|
+
);
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const resolvePasswordRecoverySessionClaims = (sessionToken, { isWebAuthJwtEnabled, verifyWebAuthJwt, passwordRecoverySessionAuthMethod, normalizeJid, normalizeEmail }) => {
|
|
141
|
+
if (!isWebAuthJwtEnabled()) {
|
|
142
|
+
return {
|
|
143
|
+
ok: false,
|
|
144
|
+
statusCode: 503,
|
|
145
|
+
error: 'JWT de autenticacao nao configurado no servidor.',
|
|
146
|
+
code: 'JWT_NOT_CONFIGURED',
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const normalizedSessionToken = normalizePasswordRecoverySessionToken(sessionToken);
|
|
151
|
+
if (!normalizedSessionToken) {
|
|
152
|
+
return {
|
|
153
|
+
ok: false,
|
|
154
|
+
statusCode: 400,
|
|
155
|
+
error: 'Sessao de redefinicao invalida.',
|
|
156
|
+
code: 'PASSWORD_RECOVERY_SESSION_INVALID',
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const claims = verifyWebAuthJwt(normalizedSessionToken);
|
|
161
|
+
if (!claims?.sub || claims.amr !== passwordRecoverySessionAuthMethod) {
|
|
162
|
+
return {
|
|
163
|
+
ok: false,
|
|
164
|
+
statusCode: 401,
|
|
165
|
+
error: 'Sessao de redefinicao invalida ou expirada.',
|
|
166
|
+
code: 'PASSWORD_RECOVERY_SESSION_EXPIRED',
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const normalizedOwnerJid = normalizeJid(claims.owner_jid || '');
|
|
171
|
+
const normalizedEmail = normalizeEmail(claims.email || '');
|
|
172
|
+
if (!normalizedOwnerJid || !normalizedEmail || !normalizedEmail.includes('@')) {
|
|
173
|
+
return {
|
|
174
|
+
ok: false,
|
|
175
|
+
statusCode: 401,
|
|
176
|
+
error: 'Sessao de redefinicao invalida.',
|
|
177
|
+
code: 'PASSWORD_RECOVERY_SESSION_INVALID',
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
ok: true,
|
|
183
|
+
claims,
|
|
184
|
+
identity: {
|
|
185
|
+
googleSub: claims.sub,
|
|
186
|
+
email: normalizedEmail,
|
|
187
|
+
ownerJid: normalizedOwnerJid,
|
|
188
|
+
purpose: 'reset',
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
const toObjectDetailsIfAny = (error) => {
|
|
194
|
+
if (Array.isArray(error?.details)) return error.details;
|
|
195
|
+
if (error?.details && typeof error.details === 'object') return error.details;
|
|
196
|
+
return undefined;
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const isUnknownColumnError = (error, columnName = '') => {
|
|
200
|
+
const code = String(error?.code || '').toUpperCase();
|
|
201
|
+
const errno = Number(error?.errno || 0);
|
|
202
|
+
if (code !== 'ER_BAD_FIELD_ERROR' && errno !== 1054) return false;
|
|
203
|
+
if (!columnName) return true;
|
|
204
|
+
const message = String(error?.message || '').toLowerCase();
|
|
205
|
+
const normalizedColumn = String(columnName || '')
|
|
206
|
+
.trim()
|
|
207
|
+
.toLowerCase();
|
|
208
|
+
if (!normalizedColumn) return true;
|
|
209
|
+
return message.includes(`unknown column '${normalizedColumn}'`) || message.includes(`unknown column \`${normalizedColumn}\``);
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const buildHttpError = (message, { statusCode = 400, code = 'BAD_REQUEST' } = {}) => {
|
|
213
|
+
const error = new Error(String(message || 'Erro interno.'));
|
|
214
|
+
error.statusCode = Number(statusCode) || 400;
|
|
215
|
+
error.code = String(code || 'BAD_REQUEST');
|
|
216
|
+
return error;
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
export const createWebAccountAuthHandlers = ({ sendJson, readJsonBody, logger, parseUserPasswordUpsertPayload, parseUserPasswordRecoveryRequestPayload, parseUserPasswordRecoveryVerifyPayload, parseUserPasswordLoginPayload, resolveGoogleWebSessionFromRequest, mapGoogleSessionResponseData, revokeGoogleWebSessionsByIdentity, createPersistedGoogleWebSessionFromIdentity, setGoogleWebSessionCookie, issueAccessTokenForSession, userPasswordAuthService, userPasswordRecoveryService, resolveRequestRemoteIp, normalizeEmail, normalizeGoogleSubject, normalizeJid, isWebAuthJwtEnabled, signWebAuthJwt, verifyWebAuthJwt, passwordRecoverySessionAuthMethod, passwordRecoverySessionTtlSeconds, userProfileWebPath, userPasswordResetWebPath, toSiteAbsoluteUrl, executeQuery, tables, toIsoOrNull, sanitizeText, listStickerPacksByOwner, listStickerPackEngagementByPackIds, mapPackSummary, isPackPubliclyVisible, resolveMyProfileOwnerCandidates, shouldHidePackFromMyProfileDefault, parseEnvBool, clampInt, stickerWebGoogleClientId, stickerWebGoogleAuthRequired, toWhatsAppPhoneDigits, getActiveSocket: _getActiveSocket, profilePictureUrlFromActiveSocket }) => {
|
|
220
|
+
const passwordPolicy = typeof userPasswordAuthService?.getPolicy === 'function' ? userPasswordAuthService.getPolicy() : {};
|
|
221
|
+
const passwordLoginIdentityMaxAttempts = clampInt(process.env.WEB_USER_PASSWORD_LOGIN_IDENTITY_MAX_ATTEMPTS, Number(passwordPolicy?.maxFailedAttempts || 8) || 8, 3, 100);
|
|
222
|
+
const passwordLoginIdentityLockoutSeconds = clampInt(process.env.WEB_USER_PASSWORD_LOGIN_IDENTITY_LOCKOUT_SECONDS, Number(passwordPolicy?.lockoutSeconds || 900) || 900, 30, 86_400);
|
|
223
|
+
const passwordLoginIdentityThrottleTable = String(tables.STICKER_WEB_USER_PASSWORD_LOGIN_THROTTLE || 'web_user_password_login_throttle').trim() || 'web_user_password_login_throttle';
|
|
224
|
+
let passwordLoginIdentityPruneAt = 0;
|
|
225
|
+
|
|
226
|
+
const buildPasswordLoginIdentityKey = ({ google_sub = '', email = '', owner_jid = '' } = {}) => {
|
|
227
|
+
const normalizedSub = normalizeGoogleSubject(google_sub);
|
|
228
|
+
const normalizedEmail = normalizeEmail(email);
|
|
229
|
+
const normalizedOwnerJid = normalizeJid(owner_jid || '') || '';
|
|
230
|
+
if (normalizedSub) return `sub:${normalizedSub}`;
|
|
231
|
+
if (normalizedEmail) return `email:${normalizedEmail}`;
|
|
232
|
+
if (normalizedOwnerJid) return `owner:${normalizedOwnerJid}`;
|
|
233
|
+
return '';
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
const maybePrunePasswordLoginIdentityThrottle = async (nowMs = Date.now()) => {
|
|
237
|
+
if (nowMs - passwordLoginIdentityPruneAt < 60 * 60 * 1000) return;
|
|
238
|
+
passwordLoginIdentityPruneAt = nowMs;
|
|
239
|
+
const staleAfterSeconds = Math.max(60 * 60, passwordLoginIdentityLockoutSeconds * 2);
|
|
240
|
+
try {
|
|
241
|
+
await executeQuery(
|
|
242
|
+
`DELETE FROM ${passwordLoginIdentityThrottleTable}
|
|
243
|
+
WHERE (locked_until IS NULL OR locked_until <= UTC_TIMESTAMP())
|
|
244
|
+
AND last_failed_at < (UTC_TIMESTAMP() - INTERVAL ${staleAfterSeconds} SECOND)`,
|
|
245
|
+
);
|
|
246
|
+
} catch (error) {
|
|
247
|
+
logger?.warn?.('Falha ao limpar throttle distribuido de login por identidade.', {
|
|
248
|
+
action: 'web_user_password_login_identity_prune_failed',
|
|
249
|
+
error: error?.message,
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
const getPasswordLoginIdentityLockState = async (identityKey) => {
|
|
255
|
+
const identityHash = hashPasswordLoginIdentityKey(identityKey);
|
|
256
|
+
if (!identityHash) return { locked: false, retryAfterSeconds: 0 };
|
|
257
|
+
await maybePrunePasswordLoginIdentityThrottle();
|
|
258
|
+
|
|
259
|
+
try {
|
|
260
|
+
const rows = await executeQuery(
|
|
261
|
+
`SELECT locked_until
|
|
262
|
+
FROM ${passwordLoginIdentityThrottleTable}
|
|
263
|
+
WHERE identity_hash = ?
|
|
264
|
+
LIMIT 1`,
|
|
265
|
+
[identityHash],
|
|
266
|
+
);
|
|
267
|
+
const row = Array.isArray(rows) ? rows[0] : null;
|
|
268
|
+
const lockedUntilMs = Date.parse(String(row?.locked_until || ''));
|
|
269
|
+
if (!Number.isFinite(lockedUntilMs) || lockedUntilMs <= Date.now()) {
|
|
270
|
+
return { locked: false, retryAfterSeconds: 0 };
|
|
271
|
+
}
|
|
272
|
+
return {
|
|
273
|
+
locked: true,
|
|
274
|
+
retryAfterSeconds: Math.max(1, Math.ceil((lockedUntilMs - Date.now()) / 1000)),
|
|
275
|
+
};
|
|
276
|
+
} catch (error) {
|
|
277
|
+
logger?.warn?.('Falha ao consultar throttle distribuido de login por identidade.', {
|
|
278
|
+
action: 'web_user_password_login_identity_lock_read_failed',
|
|
279
|
+
error: error?.message,
|
|
280
|
+
});
|
|
281
|
+
return { locked: false, retryAfterSeconds: 0 };
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
const registerPasswordLoginIdentityFailure = async (identityKey) => {
|
|
286
|
+
const identityHash = hashPasswordLoginIdentityKey(identityKey);
|
|
287
|
+
if (!identityHash) return { locked: false, retryAfterSeconds: 0 };
|
|
288
|
+
await maybePrunePasswordLoginIdentityThrottle();
|
|
289
|
+
|
|
290
|
+
try {
|
|
291
|
+
await executeQuery(
|
|
292
|
+
`INSERT INTO ${passwordLoginIdentityThrottleTable}
|
|
293
|
+
(identity_hash, failed_attempts, last_failed_at, locked_until)
|
|
294
|
+
VALUES (?, 1, UTC_TIMESTAMP(), NULL)
|
|
295
|
+
ON DUPLICATE KEY UPDATE
|
|
296
|
+
failed_attempts = IF(
|
|
297
|
+
locked_until IS NOT NULL AND locked_until > UTC_TIMESTAMP(),
|
|
298
|
+
failed_attempts,
|
|
299
|
+
failed_attempts + 1
|
|
300
|
+
),
|
|
301
|
+
last_failed_at = UTC_TIMESTAMP(),
|
|
302
|
+
locked_until = IF(
|
|
303
|
+
locked_until IS NOT NULL AND locked_until > UTC_TIMESTAMP(),
|
|
304
|
+
locked_until,
|
|
305
|
+
IF(
|
|
306
|
+
failed_attempts + 1 >= ?,
|
|
307
|
+
UTC_TIMESTAMP() + INTERVAL ? SECOND,
|
|
308
|
+
NULL
|
|
309
|
+
)
|
|
310
|
+
),
|
|
311
|
+
updated_at = UTC_TIMESTAMP()`,
|
|
312
|
+
[identityHash, passwordLoginIdentityMaxAttempts, passwordLoginIdentityLockoutSeconds],
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
const rows = await executeQuery(
|
|
316
|
+
`SELECT locked_until
|
|
317
|
+
FROM ${passwordLoginIdentityThrottleTable}
|
|
318
|
+
WHERE identity_hash = ?
|
|
319
|
+
LIMIT 1`,
|
|
320
|
+
[identityHash],
|
|
321
|
+
);
|
|
322
|
+
const row = Array.isArray(rows) ? rows[0] : null;
|
|
323
|
+
const lockedUntilMs = Date.parse(String(row?.locked_until || ''));
|
|
324
|
+
if (!Number.isFinite(lockedUntilMs) || lockedUntilMs <= Date.now()) {
|
|
325
|
+
return { locked: false, retryAfterSeconds: 0 };
|
|
326
|
+
}
|
|
327
|
+
return {
|
|
328
|
+
locked: true,
|
|
329
|
+
retryAfterSeconds: Math.max(1, Math.ceil((lockedUntilMs - Date.now()) / 1000)),
|
|
330
|
+
};
|
|
331
|
+
} catch (error) {
|
|
332
|
+
logger?.warn?.('Falha ao registrar tentativa no throttle distribuido de login por identidade.', {
|
|
333
|
+
action: 'web_user_password_login_identity_write_failed',
|
|
334
|
+
error: error?.message,
|
|
335
|
+
});
|
|
336
|
+
return { locked: false, retryAfterSeconds: 0 };
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
const clearPasswordLoginIdentityState = async (identityKey) => {
|
|
341
|
+
const identityHash = hashPasswordLoginIdentityKey(identityKey);
|
|
342
|
+
if (!identityHash) return;
|
|
343
|
+
await executeQuery(
|
|
344
|
+
`DELETE FROM ${passwordLoginIdentityThrottleTable}
|
|
345
|
+
WHERE identity_hash = ?`,
|
|
346
|
+
[identityHash],
|
|
347
|
+
).catch((error) => {
|
|
348
|
+
logger?.warn?.('Falha ao limpar throttle distribuido de login por identidade.', {
|
|
349
|
+
action: 'web_user_password_login_identity_clear_failed',
|
|
350
|
+
error: error?.message,
|
|
351
|
+
});
|
|
352
|
+
});
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
const resolvePasswordLoginFailureIdentityKey = (payload, authResult) => {
|
|
356
|
+
const credentialSub = normalizeGoogleSubject(authResult?.credential?.google_sub);
|
|
357
|
+
if (credentialSub) return `sub:${credentialSub}`;
|
|
358
|
+
return buildPasswordLoginIdentityKey(payload);
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
const sendPasswordLoginRateLimited = (req, res, retryAfterSeconds) => {
|
|
362
|
+
const safeRetryAfterSeconds = Math.max(1, Number(retryAfterSeconds || 0) || 1);
|
|
363
|
+
res.setHeader('Retry-After', String(safeRetryAfterSeconds));
|
|
364
|
+
sendJson(req, res, 429, {
|
|
365
|
+
error: 'Muitas tentativas de login. Aguarde alguns instantes para tentar novamente.',
|
|
366
|
+
code: 'AUTH_RATE_LIMITED',
|
|
367
|
+
});
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
const revokeSessionsByIdentityStrict = async ({ googleSub = '', email = '', ownerJid = '' } = {}, { reason = '' } = {}) => {
|
|
371
|
+
if (typeof revokeGoogleWebSessionsByIdentity !== 'function') return 0;
|
|
372
|
+
try {
|
|
373
|
+
return await revokeGoogleWebSessionsByIdentity({
|
|
374
|
+
googleSub,
|
|
375
|
+
email,
|
|
376
|
+
ownerJid,
|
|
377
|
+
});
|
|
378
|
+
} catch (error) {
|
|
379
|
+
logger.warn('Falha ao revogar sessoes web durante rotacao de credencial.', {
|
|
380
|
+
action: 'web_auth_session_revoke_failed',
|
|
381
|
+
reason: reason || 'unknown',
|
|
382
|
+
google_sub: normalizeGoogleSubject(googleSub),
|
|
383
|
+
owner_jid: normalizeJid(ownerJid || '') || null,
|
|
384
|
+
error: error?.message,
|
|
385
|
+
});
|
|
386
|
+
throw buildHttpError('Nao foi possivel revogar sessoes ativas da conta.', {
|
|
387
|
+
statusCode: 503,
|
|
388
|
+
code: 'SESSION_REVOKE_FAILED',
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
const isSessionRevokeFailure = (error) =>
|
|
394
|
+
String(error?.code || '')
|
|
395
|
+
.trim()
|
|
396
|
+
.toUpperCase() === 'SESSION_REVOKE_FAILED';
|
|
397
|
+
|
|
398
|
+
const createSessionPayloadFromCredential = async (req, res, credential, { reason = 'credential_update' } = {}) => {
|
|
399
|
+
if (!credential?.google_sub || !credential?.owner_jid) {
|
|
400
|
+
return mapGoogleSessionResponseData(null);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
await revokeSessionsByIdentityStrict(
|
|
404
|
+
{
|
|
405
|
+
googleSub: credential.google_sub,
|
|
406
|
+
email: credential.email || '',
|
|
407
|
+
ownerJid: credential.owner_jid,
|
|
408
|
+
},
|
|
409
|
+
{ reason },
|
|
410
|
+
);
|
|
411
|
+
|
|
412
|
+
const session = await createPersistedGoogleWebSessionFromIdentity({
|
|
413
|
+
sub: credential.google_sub,
|
|
414
|
+
email: credential.email || '',
|
|
415
|
+
name: credential.name || '',
|
|
416
|
+
picture: credential.picture || '',
|
|
417
|
+
ownerJid: credential.owner_jid,
|
|
418
|
+
requestMeta: {
|
|
419
|
+
remoteIp: resolveRequestRemoteIp(req),
|
|
420
|
+
userAgent: req.headers?.['user-agent'] || null,
|
|
421
|
+
},
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
setGoogleWebSessionCookie(req, res, session.token);
|
|
425
|
+
const accessToken = issueAccessTokenForSession(session);
|
|
426
|
+
return mapGoogleSessionResponseData(session, { accessToken });
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
const handlePasswordAuthRequest = async (req, res) => {
|
|
430
|
+
if (!['GET', 'HEAD', 'POST', 'DELETE'].includes(req.method || '')) {
|
|
431
|
+
sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const googleSession = await resolveGoogleWebSessionFromRequest(req);
|
|
436
|
+
if (!googleSession?.sub || !googleSession?.ownerJid) {
|
|
437
|
+
sendJson(req, res, 401, { error: 'Sessao Google invalida ou expirada.' });
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const identity = {
|
|
442
|
+
googleSub: googleSession.sub,
|
|
443
|
+
email: googleSession.email,
|
|
444
|
+
ownerJid: googleSession.ownerJid,
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
if (req.method === 'GET' || req.method === 'HEAD') {
|
|
448
|
+
const credential = await userPasswordAuthService.findCredentialByIdentity(identity, {
|
|
449
|
+
includeRevoked: true,
|
|
450
|
+
});
|
|
451
|
+
sendJson(req, res, 200, {
|
|
452
|
+
data: {
|
|
453
|
+
session: mapGoogleSessionResponseData(googleSession),
|
|
454
|
+
password: toUserPasswordStatePayload(credential),
|
|
455
|
+
},
|
|
456
|
+
});
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
if (req.method === 'DELETE') {
|
|
461
|
+
const revoked = await userPasswordAuthService.revokePasswordForIdentity(identity);
|
|
462
|
+
sendJson(req, res, 200, {
|
|
463
|
+
data: {
|
|
464
|
+
revoked: Boolean(revoked),
|
|
465
|
+
session: mapGoogleSessionResponseData(googleSession),
|
|
466
|
+
password: toUserPasswordStatePayload(revoked),
|
|
467
|
+
},
|
|
468
|
+
});
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
let payload = {};
|
|
473
|
+
try {
|
|
474
|
+
payload = await readJsonBody(req);
|
|
475
|
+
} catch (error) {
|
|
476
|
+
sendJson(req, res, Number(error?.statusCode || 400), {
|
|
477
|
+
error: error?.message || 'Body invalido.',
|
|
478
|
+
});
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
try {
|
|
483
|
+
payload = parseUserPasswordUpsertPayload(payload);
|
|
484
|
+
} catch (error) {
|
|
485
|
+
sendJson(req, res, Number(error?.statusCode || 400), {
|
|
486
|
+
error: error?.message || 'Payload de senha invalido.',
|
|
487
|
+
code: error?.code || 'INVALID_PAYLOAD',
|
|
488
|
+
details: Array.isArray(error?.details) ? error.details : undefined,
|
|
489
|
+
});
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
try {
|
|
494
|
+
const credential = await userPasswordAuthService.setPasswordForIdentity({
|
|
495
|
+
...identity,
|
|
496
|
+
password: payload.password,
|
|
497
|
+
});
|
|
498
|
+
let sessionPayload = mapGoogleSessionResponseData(null);
|
|
499
|
+
try {
|
|
500
|
+
sessionPayload = await createSessionPayloadFromCredential(req, res, credential, {
|
|
501
|
+
reason: 'password_upsert',
|
|
502
|
+
});
|
|
503
|
+
} catch (sessionError) {
|
|
504
|
+
if (isSessionRevokeFailure(sessionError)) {
|
|
505
|
+
throw sessionError;
|
|
506
|
+
}
|
|
507
|
+
logger.warn('Senha atualizada, mas falhou ao rotacionar sessao.', {
|
|
508
|
+
action: 'web_password_upsert_session_rotation_failed',
|
|
509
|
+
error: sessionError?.message,
|
|
510
|
+
google_sub: credential?.google_sub || null,
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
sendJson(req, res, 200, {
|
|
515
|
+
data: {
|
|
516
|
+
updated: true,
|
|
517
|
+
session: sessionPayload,
|
|
518
|
+
password: toUserPasswordStatePayload(credential),
|
|
519
|
+
},
|
|
520
|
+
});
|
|
521
|
+
} catch (error) {
|
|
522
|
+
sendJson(req, res, Number(error?.statusCode || 400), {
|
|
523
|
+
error: error?.message || 'Falha ao salvar senha.',
|
|
524
|
+
code: error?.code || 'PASSWORD_UPDATE_FAILED',
|
|
525
|
+
details: Array.isArray(error?.details) ? error.details : undefined,
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
};
|
|
529
|
+
|
|
530
|
+
const handlePasswordRecoveryRequest = async (req, res) => {
|
|
531
|
+
if (req.method !== 'POST') {
|
|
532
|
+
sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
let payload = {};
|
|
537
|
+
try {
|
|
538
|
+
payload = await readJsonBody(req);
|
|
539
|
+
} catch (error) {
|
|
540
|
+
sendJson(req, res, Number(error?.statusCode || 400), {
|
|
541
|
+
error: error?.message || 'Body invalido.',
|
|
542
|
+
});
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
try {
|
|
547
|
+
payload = parseUserPasswordRecoveryRequestPayload(payload);
|
|
548
|
+
} catch (error) {
|
|
549
|
+
sendJson(req, res, Number(error?.statusCode || 400), {
|
|
550
|
+
error: error?.message || 'Payload de recuperacao de senha invalido.',
|
|
551
|
+
code: error?.code || 'INVALID_PAYLOAD',
|
|
552
|
+
details: Array.isArray(error?.details) ? error.details : undefined,
|
|
553
|
+
});
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
try {
|
|
558
|
+
const result = await userPasswordRecoveryService.requestPasswordRecoveryCode({
|
|
559
|
+
googleSub: payload.google_sub,
|
|
560
|
+
email: payload.email,
|
|
561
|
+
ownerJid: payload.owner_jid,
|
|
562
|
+
purpose: payload.purpose || 'reset',
|
|
563
|
+
requestMeta: {
|
|
564
|
+
remoteIp: resolveRequestRemoteIp(req),
|
|
565
|
+
userAgent: req.headers?.['user-agent'] || null,
|
|
566
|
+
},
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
sendJson(req, res, 200, {
|
|
570
|
+
data: {
|
|
571
|
+
accepted: true,
|
|
572
|
+
queued: Boolean(result?.queued),
|
|
573
|
+
cooldown_active: Boolean(result?.cooldown_active),
|
|
574
|
+
rate_limited: Boolean(result?.rate_limited),
|
|
575
|
+
expires_in_seconds: Number(result?.expires_in_seconds || 0) || null,
|
|
576
|
+
masked_email:
|
|
577
|
+
result?.masked_email ||
|
|
578
|
+
maskEmailForResponse(payload.email, {
|
|
579
|
+
normalizeEmail,
|
|
580
|
+
}) ||
|
|
581
|
+
null,
|
|
582
|
+
},
|
|
583
|
+
});
|
|
584
|
+
} catch (error) {
|
|
585
|
+
const retryAfterSeconds = Math.max(0, Number(error?.details?.retry_after_seconds || 0));
|
|
586
|
+
if (retryAfterSeconds > 0) {
|
|
587
|
+
res.setHeader('Retry-After', String(retryAfterSeconds));
|
|
588
|
+
}
|
|
589
|
+
sendJson(req, res, Number(error?.statusCode || 400), {
|
|
590
|
+
error: error?.message || 'Falha ao solicitar codigo de verificacao.',
|
|
591
|
+
code: error?.code || 'PASSWORD_RECOVERY_REQUEST_FAILED',
|
|
592
|
+
details: toObjectDetailsIfAny(error),
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
};
|
|
596
|
+
|
|
597
|
+
const handlePasswordRecoveryVerifyRequest = async (req, res) => {
|
|
598
|
+
if (req.method !== 'POST') {
|
|
599
|
+
sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
let payload = {};
|
|
604
|
+
try {
|
|
605
|
+
payload = await readJsonBody(req);
|
|
606
|
+
} catch (error) {
|
|
607
|
+
sendJson(req, res, Number(error?.statusCode || 400), {
|
|
608
|
+
error: error?.message || 'Body invalido.',
|
|
609
|
+
});
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
try {
|
|
614
|
+
payload = parseUserPasswordRecoveryVerifyPayload(payload);
|
|
615
|
+
} catch (error) {
|
|
616
|
+
sendJson(req, res, Number(error?.statusCode || 400), {
|
|
617
|
+
error: error?.message || 'Payload de verificacao invalido.',
|
|
618
|
+
code: error?.code || 'INVALID_PAYLOAD',
|
|
619
|
+
details: Array.isArray(error?.details) ? error.details : undefined,
|
|
620
|
+
});
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
try {
|
|
625
|
+
const recoveryResult = await userPasswordRecoveryService.verifyPasswordRecoveryCode({
|
|
626
|
+
googleSub: payload.google_sub,
|
|
627
|
+
email: payload.email,
|
|
628
|
+
ownerJid: payload.owner_jid,
|
|
629
|
+
purpose: payload.purpose || '',
|
|
630
|
+
code: payload.code,
|
|
631
|
+
password: payload.password,
|
|
632
|
+
requestMeta: {
|
|
633
|
+
remoteIp: resolveRequestRemoteIp(req),
|
|
634
|
+
userAgent: req.headers?.['user-agent'] || null,
|
|
635
|
+
},
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
let sessionPayload = mapGoogleSessionResponseData(null);
|
|
639
|
+
if (recoveryResult?.credential?.google_sub && recoveryResult?.credential?.owner_jid) {
|
|
640
|
+
try {
|
|
641
|
+
sessionPayload = await createSessionPayloadFromCredential(req, res, recoveryResult.credential, {
|
|
642
|
+
reason: 'password_recovery_verify',
|
|
643
|
+
});
|
|
644
|
+
} catch (sessionError) {
|
|
645
|
+
if (isSessionRevokeFailure(sessionError)) {
|
|
646
|
+
throw sessionError;
|
|
647
|
+
}
|
|
648
|
+
logger.warn('Senha redefinida, mas sessao automatica nao foi criada.', {
|
|
649
|
+
action: 'web_password_recovery_session_create_failed',
|
|
650
|
+
error: sessionError?.message,
|
|
651
|
+
google_sub: recoveryResult?.credential?.google_sub || null,
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
sendJson(req, res, 200, {
|
|
657
|
+
data: {
|
|
658
|
+
updated: true,
|
|
659
|
+
auth_method: 'password_recovery',
|
|
660
|
+
session: sessionPayload,
|
|
661
|
+
password: toUserPasswordStatePayload(recoveryResult?.credential || null),
|
|
662
|
+
masked_email:
|
|
663
|
+
recoveryResult?.masked_email ||
|
|
664
|
+
maskEmailForResponse(payload.email, {
|
|
665
|
+
normalizeEmail,
|
|
666
|
+
}) ||
|
|
667
|
+
null,
|
|
668
|
+
},
|
|
669
|
+
});
|
|
670
|
+
} catch (error) {
|
|
671
|
+
sendJson(req, res, Number(error?.statusCode || 400), {
|
|
672
|
+
error: error?.message || 'Falha ao validar codigo de verificacao.',
|
|
673
|
+
code: error?.code || 'PASSWORD_RECOVERY_VERIFY_FAILED',
|
|
674
|
+
details: Array.isArray(error?.details) ? error.details : undefined,
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
};
|
|
678
|
+
|
|
679
|
+
const handlePasswordRecoverySessionCreateRequest = async (req, res) => {
|
|
680
|
+
if (req.method !== 'POST') {
|
|
681
|
+
sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
const googleSession = await resolveGoogleWebSessionFromRequest(req);
|
|
686
|
+
if (!googleSession?.sub || !googleSession?.ownerJid) {
|
|
687
|
+
sendJson(req, res, 401, { error: 'Sessao Google invalida ou expirada.' });
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
const normalizedEmail = normalizeEmail(googleSession.email || '');
|
|
692
|
+
if (!normalizedEmail || !normalizedEmail.includes('@')) {
|
|
693
|
+
sendJson(req, res, 400, {
|
|
694
|
+
error: 'Conta sem e-mail valido para recuperacao.',
|
|
695
|
+
code: 'PASSWORD_RECOVERY_EMAIL_REQUIRED',
|
|
696
|
+
});
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
const sessionToken = signPasswordRecoverySessionToken(
|
|
701
|
+
{
|
|
702
|
+
sub: googleSession.sub,
|
|
703
|
+
email: normalizedEmail,
|
|
704
|
+
ownerJid: googleSession.ownerJid,
|
|
705
|
+
},
|
|
706
|
+
{
|
|
707
|
+
isWebAuthJwtEnabled,
|
|
708
|
+
signWebAuthJwt,
|
|
709
|
+
passwordRecoverySessionAuthMethod,
|
|
710
|
+
passwordRecoverySessionTtlSeconds,
|
|
711
|
+
},
|
|
712
|
+
);
|
|
713
|
+
|
|
714
|
+
if (!sessionToken) {
|
|
715
|
+
sendJson(req, res, 503, {
|
|
716
|
+
error: 'JWT de autenticacao nao configurado no servidor.',
|
|
717
|
+
code: 'JWT_NOT_CONFIGURED',
|
|
718
|
+
});
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
const claims = verifyWebAuthJwt(sessionToken);
|
|
723
|
+
const sessionPath = buildPasswordRecoverySessionPath({
|
|
724
|
+
userPasswordResetWebPath,
|
|
725
|
+
userProfileWebPath,
|
|
726
|
+
});
|
|
727
|
+
const sessionUrl = toSiteAbsoluteUrl(sessionPath);
|
|
728
|
+
|
|
729
|
+
sendJson(req, res, 200, {
|
|
730
|
+
data: {
|
|
731
|
+
created: true,
|
|
732
|
+
purpose: 'reset',
|
|
733
|
+
session_token: sessionToken,
|
|
734
|
+
session_path: sessionPath,
|
|
735
|
+
session_url: sessionUrl,
|
|
736
|
+
session_path_legacy: null,
|
|
737
|
+
session_url_legacy: null,
|
|
738
|
+
masked_email: maskEmailForResponse(normalizedEmail, {
|
|
739
|
+
normalizeEmail,
|
|
740
|
+
}),
|
|
741
|
+
expires_at: toPasswordRecoverySessionExpiresAt(claims),
|
|
742
|
+
expires_in_seconds: toPasswordRecoverySessionExpiresIn(claims),
|
|
743
|
+
},
|
|
744
|
+
});
|
|
745
|
+
};
|
|
746
|
+
|
|
747
|
+
const handlePasswordRecoverySessionStatusRequest = async (req, res, { sessionToken = '' } = {}) => {
|
|
748
|
+
if (!['GET', 'HEAD'].includes(req.method || '')) {
|
|
749
|
+
sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
const resolvedSessionToken = normalizePasswordRecoverySessionToken(sessionToken) || resolvePasswordRecoverySessionTokenFromRequest(req);
|
|
754
|
+
if (!resolvedSessionToken) {
|
|
755
|
+
sendJson(req, res, 400, {
|
|
756
|
+
error: 'Sessao de redefinicao invalida.',
|
|
757
|
+
code: 'PASSWORD_RECOVERY_SESSION_INVALID',
|
|
758
|
+
});
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
const resolvedSession = resolvePasswordRecoverySessionClaims(resolvedSessionToken, {
|
|
763
|
+
isWebAuthJwtEnabled,
|
|
764
|
+
verifyWebAuthJwt,
|
|
765
|
+
passwordRecoverySessionAuthMethod,
|
|
766
|
+
normalizeJid,
|
|
767
|
+
normalizeEmail,
|
|
768
|
+
});
|
|
769
|
+
if (!resolvedSession.ok) {
|
|
770
|
+
sendJson(req, res, resolvedSession.statusCode, {
|
|
771
|
+
error: resolvedSession.error,
|
|
772
|
+
code: resolvedSession.code,
|
|
773
|
+
});
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
const credential = await userPasswordAuthService.findCredentialByIdentity(
|
|
778
|
+
{
|
|
779
|
+
googleSub: resolvedSession.identity.googleSub,
|
|
780
|
+
email: resolvedSession.identity.email,
|
|
781
|
+
ownerJid: resolvedSession.identity.ownerJid,
|
|
782
|
+
},
|
|
783
|
+
{ includeRevoked: true },
|
|
784
|
+
);
|
|
785
|
+
|
|
786
|
+
sendJson(req, res, 200, {
|
|
787
|
+
data: {
|
|
788
|
+
valid: true,
|
|
789
|
+
purpose: resolvedSession.identity.purpose,
|
|
790
|
+
masked_email: maskEmailForResponse(resolvedSession.identity.email, {
|
|
791
|
+
normalizeEmail,
|
|
792
|
+
}),
|
|
793
|
+
expires_at: toPasswordRecoverySessionExpiresAt(resolvedSession.claims),
|
|
794
|
+
expires_in_seconds: toPasswordRecoverySessionExpiresIn(resolvedSession.claims),
|
|
795
|
+
password: toUserPasswordStatePayload(credential),
|
|
796
|
+
},
|
|
797
|
+
});
|
|
798
|
+
};
|
|
799
|
+
|
|
800
|
+
const handlePasswordRecoverySessionRequest = async (req, res, { sessionToken = '' } = {}) => {
|
|
801
|
+
if (req.method !== 'POST') {
|
|
802
|
+
sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
|
|
803
|
+
return;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
let resolvedSessionToken = normalizePasswordRecoverySessionToken(sessionToken) || resolvePasswordRecoverySessionTokenFromRequest(req);
|
|
807
|
+
if (!resolvedSessionToken) {
|
|
808
|
+
try {
|
|
809
|
+
const payload = await readJsonBody(req);
|
|
810
|
+
resolvedSessionToken = resolvePasswordRecoverySessionTokenFromRequest(req, payload);
|
|
811
|
+
} catch (error) {
|
|
812
|
+
sendJson(req, res, Number(error?.statusCode || 400), {
|
|
813
|
+
error: error?.message || 'Body invalido.',
|
|
814
|
+
});
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
if (!resolvedSessionToken) {
|
|
820
|
+
sendJson(req, res, 400, {
|
|
821
|
+
error: 'Sessao de redefinicao invalida.',
|
|
822
|
+
code: 'PASSWORD_RECOVERY_SESSION_INVALID',
|
|
823
|
+
});
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
const resolvedSession = resolvePasswordRecoverySessionClaims(resolvedSessionToken, {
|
|
828
|
+
isWebAuthJwtEnabled,
|
|
829
|
+
verifyWebAuthJwt,
|
|
830
|
+
passwordRecoverySessionAuthMethod,
|
|
831
|
+
normalizeJid,
|
|
832
|
+
normalizeEmail,
|
|
833
|
+
});
|
|
834
|
+
if (!resolvedSession.ok) {
|
|
835
|
+
sendJson(req, res, resolvedSession.statusCode, {
|
|
836
|
+
error: resolvedSession.error,
|
|
837
|
+
code: resolvedSession.code,
|
|
838
|
+
});
|
|
839
|
+
return;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
try {
|
|
843
|
+
const result = await userPasswordRecoveryService.requestPasswordRecoveryCode({
|
|
844
|
+
googleSub: resolvedSession.identity.googleSub,
|
|
845
|
+
email: resolvedSession.identity.email,
|
|
846
|
+
ownerJid: resolvedSession.identity.ownerJid,
|
|
847
|
+
purpose: resolvedSession.identity.purpose,
|
|
848
|
+
requestMeta: {
|
|
849
|
+
remoteIp: resolveRequestRemoteIp(req),
|
|
850
|
+
userAgent: req.headers?.['user-agent'] || null,
|
|
851
|
+
},
|
|
852
|
+
});
|
|
853
|
+
|
|
854
|
+
sendJson(req, res, 200, {
|
|
855
|
+
data: {
|
|
856
|
+
accepted: true,
|
|
857
|
+
queued: Boolean(result?.queued),
|
|
858
|
+
cooldown_active: Boolean(result?.cooldown_active),
|
|
859
|
+
rate_limited: Boolean(result?.rate_limited),
|
|
860
|
+
expires_in_seconds: Number(result?.expires_in_seconds || 0) || null,
|
|
861
|
+
masked_email:
|
|
862
|
+
result?.masked_email ||
|
|
863
|
+
maskEmailForResponse(resolvedSession.identity.email, {
|
|
864
|
+
normalizeEmail,
|
|
865
|
+
}) ||
|
|
866
|
+
null,
|
|
867
|
+
},
|
|
868
|
+
});
|
|
869
|
+
} catch (error) {
|
|
870
|
+
const retryAfterSeconds = Math.max(0, Number(error?.details?.retry_after_seconds || 0));
|
|
871
|
+
if (retryAfterSeconds > 0) {
|
|
872
|
+
res.setHeader('Retry-After', String(retryAfterSeconds));
|
|
873
|
+
}
|
|
874
|
+
sendJson(req, res, Number(error?.statusCode || 400), {
|
|
875
|
+
error: error?.message || 'Falha ao solicitar codigo de verificacao.',
|
|
876
|
+
code: error?.code || 'PASSWORD_RECOVERY_REQUEST_FAILED',
|
|
877
|
+
details: toObjectDetailsIfAny(error),
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
};
|
|
881
|
+
|
|
882
|
+
const handlePasswordRecoverySessionVerifyRequest = async (req, res, { sessionToken = '' } = {}) => {
|
|
883
|
+
if (req.method !== 'POST') {
|
|
884
|
+
sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
|
|
885
|
+
return;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
let payload = {};
|
|
889
|
+
try {
|
|
890
|
+
payload = await readJsonBody(req);
|
|
891
|
+
} catch (error) {
|
|
892
|
+
sendJson(req, res, Number(error?.statusCode || 400), {
|
|
893
|
+
error: error?.message || 'Body invalido.',
|
|
894
|
+
});
|
|
895
|
+
return;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
const resolvedSessionToken = normalizePasswordRecoverySessionToken(sessionToken) || resolvePasswordRecoverySessionTokenFromRequest(req, payload);
|
|
899
|
+
if (!resolvedSessionToken) {
|
|
900
|
+
sendJson(req, res, 400, {
|
|
901
|
+
error: 'Sessao de redefinicao invalida.',
|
|
902
|
+
code: 'PASSWORD_RECOVERY_SESSION_INVALID',
|
|
903
|
+
});
|
|
904
|
+
return;
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
const resolvedSession = resolvePasswordRecoverySessionClaims(resolvedSessionToken, {
|
|
908
|
+
isWebAuthJwtEnabled,
|
|
909
|
+
verifyWebAuthJwt,
|
|
910
|
+
passwordRecoverySessionAuthMethod,
|
|
911
|
+
normalizeJid,
|
|
912
|
+
normalizeEmail,
|
|
913
|
+
});
|
|
914
|
+
if (!resolvedSession.ok) {
|
|
915
|
+
sendJson(req, res, resolvedSession.statusCode, {
|
|
916
|
+
error: resolvedSession.error,
|
|
917
|
+
code: resolvedSession.code,
|
|
918
|
+
});
|
|
919
|
+
return;
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
try {
|
|
923
|
+
payload = parseUserPasswordRecoveryVerifyPayload({
|
|
924
|
+
...payload,
|
|
925
|
+
google_sub: resolvedSession.identity.googleSub,
|
|
926
|
+
email: resolvedSession.identity.email,
|
|
927
|
+
owner_jid: resolvedSession.identity.ownerJid,
|
|
928
|
+
purpose: resolvedSession.identity.purpose,
|
|
929
|
+
});
|
|
930
|
+
} catch (error) {
|
|
931
|
+
sendJson(req, res, Number(error?.statusCode || 400), {
|
|
932
|
+
error: error?.message || 'Payload de verificacao invalido.',
|
|
933
|
+
code: error?.code || 'INVALID_PAYLOAD',
|
|
934
|
+
details: Array.isArray(error?.details) ? error.details : undefined,
|
|
935
|
+
});
|
|
936
|
+
return;
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
try {
|
|
940
|
+
const recoveryResult = await userPasswordRecoveryService.verifyPasswordRecoveryCode({
|
|
941
|
+
googleSub: payload.google_sub,
|
|
942
|
+
email: payload.email,
|
|
943
|
+
ownerJid: payload.owner_jid,
|
|
944
|
+
purpose: payload.purpose || '',
|
|
945
|
+
code: payload.code,
|
|
946
|
+
password: payload.password,
|
|
947
|
+
requestMeta: {
|
|
948
|
+
remoteIp: resolveRequestRemoteIp(req),
|
|
949
|
+
userAgent: req.headers?.['user-agent'] || null,
|
|
950
|
+
},
|
|
951
|
+
});
|
|
952
|
+
|
|
953
|
+
let sessionPayload = mapGoogleSessionResponseData(null);
|
|
954
|
+
if (recoveryResult?.credential?.google_sub && recoveryResult?.credential?.owner_jid) {
|
|
955
|
+
try {
|
|
956
|
+
sessionPayload = await createSessionPayloadFromCredential(req, res, recoveryResult.credential, {
|
|
957
|
+
reason: 'password_recovery_session_verify',
|
|
958
|
+
});
|
|
959
|
+
} catch (sessionError) {
|
|
960
|
+
if (isSessionRevokeFailure(sessionError)) {
|
|
961
|
+
throw sessionError;
|
|
962
|
+
}
|
|
963
|
+
logger.warn('Senha redefinida por sessao, mas login automatico nao foi criado.', {
|
|
964
|
+
action: 'web_password_recovery_session_login_create_failed',
|
|
965
|
+
error: sessionError?.message,
|
|
966
|
+
google_sub: recoveryResult?.credential?.google_sub || null,
|
|
967
|
+
});
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
sendJson(req, res, 200, {
|
|
972
|
+
data: {
|
|
973
|
+
updated: true,
|
|
974
|
+
auth_method: 'password_recovery_session',
|
|
975
|
+
session: sessionPayload,
|
|
976
|
+
password: toUserPasswordStatePayload(recoveryResult?.credential || null),
|
|
977
|
+
masked_email:
|
|
978
|
+
recoveryResult?.masked_email ||
|
|
979
|
+
maskEmailForResponse(payload.email, {
|
|
980
|
+
normalizeEmail,
|
|
981
|
+
}) ||
|
|
982
|
+
null,
|
|
983
|
+
},
|
|
984
|
+
});
|
|
985
|
+
} catch (error) {
|
|
986
|
+
sendJson(req, res, Number(error?.statusCode || 400), {
|
|
987
|
+
error: error?.message || 'Falha ao validar codigo de verificacao.',
|
|
988
|
+
code: error?.code || 'PASSWORD_RECOVERY_VERIFY_FAILED',
|
|
989
|
+
details: Array.isArray(error?.details) ? error.details : undefined,
|
|
990
|
+
});
|
|
991
|
+
}
|
|
992
|
+
};
|
|
993
|
+
|
|
994
|
+
const handlePasswordLoginRequest = async (req, res) => {
|
|
995
|
+
if (req.method !== 'POST') {
|
|
996
|
+
sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
let payload = {};
|
|
1001
|
+
try {
|
|
1002
|
+
payload = await readJsonBody(req);
|
|
1003
|
+
} catch (error) {
|
|
1004
|
+
sendJson(req, res, Number(error?.statusCode || 400), {
|
|
1005
|
+
error: error?.message || 'Body invalido.',
|
|
1006
|
+
});
|
|
1007
|
+
return;
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
try {
|
|
1011
|
+
payload = parseUserPasswordLoginPayload(payload);
|
|
1012
|
+
} catch (error) {
|
|
1013
|
+
sendJson(req, res, Number(error?.statusCode || 400), {
|
|
1014
|
+
error: error?.message || 'Payload de login por senha invalido.',
|
|
1015
|
+
code: error?.code || 'INVALID_PAYLOAD',
|
|
1016
|
+
details: Array.isArray(error?.details) ? error.details : undefined,
|
|
1017
|
+
});
|
|
1018
|
+
return;
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
const loginIdentityKey = buildPasswordLoginIdentityKey(payload);
|
|
1022
|
+
const localLockState = await getPasswordLoginIdentityLockState(loginIdentityKey);
|
|
1023
|
+
if (localLockState.locked) {
|
|
1024
|
+
sendPasswordLoginRateLimited(req, res, localLockState.retryAfterSeconds);
|
|
1025
|
+
return;
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
const authResult = await userPasswordAuthService.verifyPasswordForIdentity({
|
|
1029
|
+
googleSub: payload.google_sub,
|
|
1030
|
+
email: payload.email,
|
|
1031
|
+
ownerJid: payload.owner_jid,
|
|
1032
|
+
password: payload.password,
|
|
1033
|
+
});
|
|
1034
|
+
|
|
1035
|
+
if (!authResult?.authenticated || !authResult?.credential?.google_sub || !authResult?.credential?.owner_jid) {
|
|
1036
|
+
const failedIdentityKey = resolvePasswordLoginFailureIdentityKey(payload, authResult);
|
|
1037
|
+
const failedLockState = await registerPasswordLoginIdentityFailure(failedIdentityKey);
|
|
1038
|
+
const retryAfterSeconds = Math.max(Number(authResult?.retryAfterSeconds || 0), Number(failedLockState.retryAfterSeconds || 0));
|
|
1039
|
+
if (retryAfterSeconds > 0) {
|
|
1040
|
+
sendPasswordLoginRateLimited(req, res, retryAfterSeconds);
|
|
1041
|
+
return;
|
|
1042
|
+
}
|
|
1043
|
+
sendJson(req, res, 401, {
|
|
1044
|
+
error: 'Credenciais invalidas.',
|
|
1045
|
+
code: 'INVALID_CREDENTIALS',
|
|
1046
|
+
});
|
|
1047
|
+
return;
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
try {
|
|
1051
|
+
const credential = authResult.credential;
|
|
1052
|
+
await clearPasswordLoginIdentityState(loginIdentityKey);
|
|
1053
|
+
const credentialSubKey = normalizeGoogleSubject(credential.google_sub);
|
|
1054
|
+
if (credentialSubKey) {
|
|
1055
|
+
await clearPasswordLoginIdentityState(`sub:${credentialSubKey}`);
|
|
1056
|
+
}
|
|
1057
|
+
const session = await createPersistedGoogleWebSessionFromIdentity({
|
|
1058
|
+
sub: credential.google_sub,
|
|
1059
|
+
email: credential.email || '',
|
|
1060
|
+
name: credential.name || '',
|
|
1061
|
+
picture: credential.picture || '',
|
|
1062
|
+
ownerJid: credential.owner_jid,
|
|
1063
|
+
requestMeta: {
|
|
1064
|
+
remoteIp: resolveRequestRemoteIp(req),
|
|
1065
|
+
userAgent: req.headers?.['user-agent'] || null,
|
|
1066
|
+
},
|
|
1067
|
+
});
|
|
1068
|
+
|
|
1069
|
+
setGoogleWebSessionCookie(req, res, session.token);
|
|
1070
|
+
const accessToken = issueAccessTokenForSession(session);
|
|
1071
|
+
sendJson(req, res, 200, {
|
|
1072
|
+
data: {
|
|
1073
|
+
auth_method: 'password',
|
|
1074
|
+
session: mapGoogleSessionResponseData(session, { accessToken }),
|
|
1075
|
+
password: toUserPasswordStatePayload(credential),
|
|
1076
|
+
},
|
|
1077
|
+
});
|
|
1078
|
+
} catch (error) {
|
|
1079
|
+
sendJson(req, res, Number(error?.statusCode || 500), {
|
|
1080
|
+
error: error?.message || 'Falha ao finalizar login por senha.',
|
|
1081
|
+
code: error?.code || 'PASSWORD_LOGIN_FAILED',
|
|
1082
|
+
});
|
|
1083
|
+
}
|
|
1084
|
+
};
|
|
1085
|
+
|
|
1086
|
+
const resolveMyProfileAccountSummary = async (session) => {
|
|
1087
|
+
if (!session) return null;
|
|
1088
|
+
|
|
1089
|
+
const planLabel = sanitizeText(process.env.STICKER_WEB_USER_PLAN_LABEL || '', 80, { allowEmpty: true }) || 'Conta padrao';
|
|
1090
|
+
const normalizedSub = normalizeGoogleSubject(session?.sub);
|
|
1091
|
+
const normalizedEmail = normalizeEmail(session?.email);
|
|
1092
|
+
const normalizedOwnerJid = normalizeJid(session?.ownerJid || '') || '';
|
|
1093
|
+
const identityClauses = [];
|
|
1094
|
+
const identityParams = [];
|
|
1095
|
+
|
|
1096
|
+
if (normalizedSub) {
|
|
1097
|
+
identityClauses.push('google_sub = ?');
|
|
1098
|
+
identityParams.push(normalizedSub);
|
|
1099
|
+
}
|
|
1100
|
+
if (normalizedEmail) {
|
|
1101
|
+
identityClauses.push('email = ?');
|
|
1102
|
+
identityParams.push(normalizedEmail);
|
|
1103
|
+
}
|
|
1104
|
+
if (normalizedOwnerJid) {
|
|
1105
|
+
identityClauses.push('owner_jid = ?');
|
|
1106
|
+
identityParams.push(normalizedOwnerJid);
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
let lastLoginAt = null;
|
|
1110
|
+
let lastSeenAt = null;
|
|
1111
|
+
let dbOwnerJid = null;
|
|
1112
|
+
let dbPicture = null;
|
|
1113
|
+
|
|
1114
|
+
if (identityClauses.length) {
|
|
1115
|
+
try {
|
|
1116
|
+
let rows = null;
|
|
1117
|
+
try {
|
|
1118
|
+
rows = await executeQuery('SELECT last_login_at, last_seen_at, owner_jid, picture_url AS picture FROM ' + tables.STICKER_WEB_GOOGLE_USER + ' WHERE ' + identityClauses.join(' OR ') + ' ORDER BY COALESCE(last_login_at, last_seen_at, updated_at, created_at) DESC LIMIT 1', identityParams);
|
|
1119
|
+
} catch (error) {
|
|
1120
|
+
if (!isUnknownColumnError(error, 'picture_url')) throw error;
|
|
1121
|
+
rows = await executeQuery('SELECT last_login_at, last_seen_at, owner_jid, picture FROM ' + tables.STICKER_WEB_GOOGLE_USER + ' WHERE ' + identityClauses.join(' OR ') + ' ORDER BY COALESCE(last_login_at, last_seen_at, updated_at, created_at) DESC LIMIT 1', identityParams);
|
|
1122
|
+
}
|
|
1123
|
+
const entry = Array.isArray(rows) ? rows[0] : null;
|
|
1124
|
+
lastLoginAt = toIsoOrNull(entry?.last_login_at);
|
|
1125
|
+
lastSeenAt = toIsoOrNull(entry?.last_seen_at);
|
|
1126
|
+
dbOwnerJid = normalizeJid(entry?.owner_jid);
|
|
1127
|
+
dbPicture = entry?.picture;
|
|
1128
|
+
} catch (error) {
|
|
1129
|
+
logger.warn('Falha ao resolver resumo de conta do perfil web.', { error: error?.message });
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
const effectiveOwnerJid = normalizedOwnerJid || dbOwnerJid;
|
|
1134
|
+
let rpg = null;
|
|
1135
|
+
const usage = { messages: 0, packs: 0, stickers: 0, activity_chart: [], insights: {} };
|
|
1136
|
+
|
|
1137
|
+
if (effectiveOwnerJid) {
|
|
1138
|
+
try {
|
|
1139
|
+
// Basic RPG Info
|
|
1140
|
+
const rpgRows = await executeQuery('SELECT level, xp, gold, created_at, updated_at FROM ' + tables.RPG_PLAYER + ' WHERE jid = ? LIMIT 1', [effectiveOwnerJid]);
|
|
1141
|
+
const rpgRow = rpgRows?.[0];
|
|
1142
|
+
|
|
1143
|
+
if (rpgRow) {
|
|
1144
|
+
const activePokemonRows = await executeQuery('SELECT poke_id, nickname, level, is_shiny FROM ' + tables.RPG_PLAYER_POKEMON + ' WHERE owner_jid = ? AND is_active = 1 LIMIT 1', [effectiveOwnerJid]);
|
|
1145
|
+
const pokemonCountRows = await executeQuery('SELECT COUNT(*) as total FROM ' + tables.RPG_PLAYER_POKEMON + ' WHERE owner_jid = ?', [effectiveOwnerJid]);
|
|
1146
|
+
const pvpStatsRows = await executeQuery('SELECT COALESCE(SUM(matches_played), 0) AS matches_played, COALESCE(SUM(wins), 0) AS wins, COALESCE(SUM(losses), 0) AS losses FROM ' + tables.RPG_PVP_WEEKLY_STATS + ' WHERE owner_jid = ?', [effectiveOwnerJid]);
|
|
1147
|
+
const karmaRows = await executeQuery('SELECT karma_score, positive_votes, negative_votes FROM ' + tables.RPG_KARMA_PROFILE + ' WHERE owner_jid = ? LIMIT 1', [effectiveOwnerJid]);
|
|
1148
|
+
const inventoryRows = await executeQuery('SELECT COUNT(*) as total FROM ' + tables.RPG_PLAYER_INVENTORY + ' WHERE owner_jid = ?', [effectiveOwnerJid]);
|
|
1149
|
+
|
|
1150
|
+
const karmaRow = karmaRows?.[0];
|
|
1151
|
+
const pvpRow = pvpStatsRows?.[0];
|
|
1152
|
+
|
|
1153
|
+
rpg = {
|
|
1154
|
+
level: Number(rpgRow.level || 1),
|
|
1155
|
+
xp: Number(rpgRow.xp || 0),
|
|
1156
|
+
gold: Number(rpgRow.gold || 0),
|
|
1157
|
+
member_since: toIsoOrNull(rpgRow.created_at),
|
|
1158
|
+
active_pokemon: activePokemonRows?.[0] || null,
|
|
1159
|
+
total_pokemons: Number(pokemonCountRows?.[0]?.total || 0),
|
|
1160
|
+
inventory_count: Number(inventoryRows?.[0]?.total || 0),
|
|
1161
|
+
karma: karmaRow
|
|
1162
|
+
? {
|
|
1163
|
+
score: Number(karmaRow.karma_score),
|
|
1164
|
+
positive: Number(karmaRow.positive_votes),
|
|
1165
|
+
negative: Number(karmaRow.negative_votes),
|
|
1166
|
+
}
|
|
1167
|
+
: { score: 0, positive: 0, negative: 0 },
|
|
1168
|
+
pvp: {
|
|
1169
|
+
matches: Number(pvpRow?.matches_played || 0),
|
|
1170
|
+
wins: Number(pvpRow?.wins || 0),
|
|
1171
|
+
losses: Number(pvpRow?.losses || 0),
|
|
1172
|
+
},
|
|
1173
|
+
};
|
|
1174
|
+
const rpgLastSeen = toIsoOrNull(rpgRow.updated_at);
|
|
1175
|
+
if (rpgLastSeen && (!lastSeenAt || new Date(rpgLastSeen) > new Date(lastSeenAt))) {
|
|
1176
|
+
lastSeenAt = rpgLastSeen;
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
// Usage Stats
|
|
1181
|
+
const msgStatsRows = await executeQuery('SELECT COUNT(*) as total, MIN(timestamp) as first_msg, MAX(timestamp) as last_msg FROM ' + tables.MESSAGES + ' WHERE canonical_sender_id = ? OR sender_id = ?', [effectiveOwnerJid, effectiveOwnerJid]);
|
|
1182
|
+
const msgStats = msgStatsRows?.[0];
|
|
1183
|
+
usage.messages = Number(msgStats?.total || 0);
|
|
1184
|
+
usage.first_message_at = toIsoOrNull(msgStats?.first_msg);
|
|
1185
|
+
usage.last_message_at = toIsoOrNull(msgStats?.last_msg);
|
|
1186
|
+
|
|
1187
|
+
const commandStatsRows = await executeQuery('SELECT COUNT(*) as total FROM ' + tables.MESSAGES + " WHERE (canonical_sender_id = ? OR sender_id = ?) AND content LIKE '/%'", [effectiveOwnerJid, effectiveOwnerJid]);
|
|
1188
|
+
|
|
1189
|
+
const topCommandRows = await executeQuery("SELECT SUBSTRING_INDEX(SUBSTRING_INDEX(content, ' ', 1), '\\n', 1) as cmd, COUNT(*) as total FROM " + tables.MESSAGES + " WHERE (canonical_sender_id = ? OR sender_id = ?) AND content LIKE '/%' GROUP BY cmd ORDER BY total DESC LIMIT 1", [effectiveOwnerJid, effectiveOwnerJid]);
|
|
1190
|
+
|
|
1191
|
+
const typeSql = "(CASE WHEN JSON_EXTRACT(raw_message, '$.message.conversation') IS NOT NULL THEN 'texto' WHEN JSON_EXTRACT(raw_message, '$.message.extendedTextMessage') IS NOT NULL THEN 'texto' WHEN JSON_EXTRACT(raw_message, '$.message.imageMessage') IS NOT NULL THEN 'imagem' WHEN JSON_EXTRACT(raw_message, '$.message.videoMessage') IS NOT NULL THEN 'video' WHEN JSON_EXTRACT(raw_message, '$.message.audioMessage') IS NOT NULL THEN 'audio' WHEN JSON_EXTRACT(raw_message, '$.message.stickerMessage') IS NOT NULL THEN 'figurinha' WHEN JSON_EXTRACT(raw_message, '$.message.documentMessage') IS NOT NULL THEN 'documento' WHEN JSON_EXTRACT(raw_message, '$.message.reactionMessage') IS NOT NULL THEN 'reacao' ELSE 'outros' END)";
|
|
1192
|
+
const topTypeRows = await executeQuery('SELECT ' + typeSql + ' as type, COUNT(*) as total FROM ' + tables.MESSAGES + ' WHERE canonical_sender_id = ? OR sender_id = ? GROUP BY type ORDER BY total DESC LIMIT 1', [effectiveOwnerJid, effectiveOwnerJid]);
|
|
1193
|
+
|
|
1194
|
+
const groupsCountRows = await executeQuery('SELECT COUNT(DISTINCT chat_id) as total FROM ' + tables.MESSAGES + " WHERE (canonical_sender_id = ? OR sender_id = ?) AND chat_id LIKE '%@g.us'", [effectiveOwnerJid, effectiveOwnerJid]);
|
|
1195
|
+
|
|
1196
|
+
const topGroupRows = await executeQuery('SELECT m.chat_id, COALESCE(gm.subject, m.chat_id) as name, COUNT(*) as total FROM ' + tables.MESSAGES + ' m LEFT JOIN ' + tables.GROUPS_METADATA + " gm ON gm.id = m.chat_id WHERE (m.canonical_sender_id = ? OR m.sender_id = ?) AND m.chat_id LIKE '%@g.us' GROUP BY m.chat_id, gm.subject ORDER BY total DESC LIMIT 1", [effectiveOwnerJid, effectiveOwnerJid]);
|
|
1197
|
+
|
|
1198
|
+
const activeHourRows = await executeQuery('SELECT HOUR(timestamp) as hour, COUNT(*) as total FROM ' + tables.MESSAGES + ' WHERE canonical_sender_id = ? OR sender_id = ? GROUP BY hour ORDER BY total DESC LIMIT 1', [effectiveOwnerJid, effectiveOwnerJid]);
|
|
1199
|
+
|
|
1200
|
+
const chartRows = await executeQuery('SELECT DATE(timestamp) as day, COUNT(*) as count FROM ' + tables.MESSAGES + ' WHERE (canonical_sender_id = ? OR sender_id = ?) AND timestamp >= NOW() - INTERVAL 7 DAY GROUP BY DATE(timestamp) ORDER BY day ASC', [effectiveOwnerJid, effectiveOwnerJid]);
|
|
1201
|
+
|
|
1202
|
+
let avgDaily = 0;
|
|
1203
|
+
if (usage.messages > 0 && usage.first_message_at) {
|
|
1204
|
+
const daysDiff = Math.max(1, (Date.now() - new Date(usage.first_message_at).getTime()) / (1000 * 60 * 60 * 24));
|
|
1205
|
+
avgDaily = (usage.messages / daysDiff).toFixed(2);
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
usage.insights = {
|
|
1209
|
+
commands_total: Number(commandStatsRows?.[0]?.total || 0),
|
|
1210
|
+
top_command: topCommandRows?.[0]?.cmd || 'N/D',
|
|
1211
|
+
top_command_count: Number(topCommandRows?.[0]?.total || 0),
|
|
1212
|
+
top_message_type: topTypeRows?.[0]?.type || 'texto',
|
|
1213
|
+
groups_active: Number(groupsCountRows?.[0]?.total || 0),
|
|
1214
|
+
top_group: topGroupRows?.[0]?.name || 'N/D',
|
|
1215
|
+
active_hour: activeHourRows?.[0]?.hour ?? null,
|
|
1216
|
+
avg_daily: avgDaily,
|
|
1217
|
+
};
|
|
1218
|
+
|
|
1219
|
+
usage.activity_chart = (chartRows || []).map((r) => ({
|
|
1220
|
+
day: r.day instanceof Date ? r.day.toISOString().slice(5, 10) : String(r.day).slice(5, 10),
|
|
1221
|
+
count: Number(r.count),
|
|
1222
|
+
}));
|
|
1223
|
+
} catch (error) {
|
|
1224
|
+
logger.warn('Falha ao buscar estatisticas expandidas do usuario.', { owner_jid: effectiveOwnerJid, error: error?.message });
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
if (effectiveOwnerJid) {
|
|
1229
|
+
const packRows = await executeQuery('SELECT COUNT(DISTINCT p.id) AS packs, COUNT(i.sticker_id) AS stickers FROM ' + tables.STICKER_PACK + ' p LEFT JOIN ' + tables.STICKER_PACK_ITEM + ' i ON i.pack_id = p.id WHERE p.owner_jid = ? AND p.deleted_at IS NULL', [effectiveOwnerJid]);
|
|
1230
|
+
usage.packs = Number(packRows?.[0]?.packs || 0);
|
|
1231
|
+
usage.stickers = Number(packRows?.[0]?.stickers || 0);
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
// Resolve Profile Picture
|
|
1235
|
+
let picture = dbPicture || session?.user?.picture;
|
|
1236
|
+
const isGeneric = !picture || picture.includes('brand-logo');
|
|
1237
|
+
|
|
1238
|
+
if (effectiveOwnerJid && isGeneric) {
|
|
1239
|
+
try {
|
|
1240
|
+
const waPicture = await profilePictureUrlFromActiveSocket(effectiveOwnerJid, 'image', 3000);
|
|
1241
|
+
if (waPicture) {
|
|
1242
|
+
picture = waPicture;
|
|
1243
|
+
}
|
|
1244
|
+
} catch {
|
|
1245
|
+
logger.debug('Falha ao buscar foto de perfil do WhatsApp.', { jid: effectiveOwnerJid });
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
return {
|
|
1250
|
+
plan_label: planLabel,
|
|
1251
|
+
status: 'active',
|
|
1252
|
+
last_login_at: lastLoginAt,
|
|
1253
|
+
last_seen_at: lastSeenAt,
|
|
1254
|
+
rpg,
|
|
1255
|
+
usage,
|
|
1256
|
+
owner_phone: toWhatsAppPhoneDigits(effectiveOwnerJid),
|
|
1257
|
+
picture: picture || null,
|
|
1258
|
+
};
|
|
1259
|
+
};
|
|
1260
|
+
|
|
1261
|
+
const handleMyProfileRequest = async (req, res, url = null) => {
|
|
1262
|
+
if (!['GET', 'HEAD'].includes(req.method || '')) {
|
|
1263
|
+
sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
|
|
1264
|
+
return;
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
const session = await resolveGoogleWebSessionFromRequest(req);
|
|
1268
|
+
const authGoogle = {
|
|
1269
|
+
enabled: Boolean(stickerWebGoogleClientId),
|
|
1270
|
+
required: Boolean(stickerWebGoogleAuthRequired),
|
|
1271
|
+
client_id: stickerWebGoogleClientId || null,
|
|
1272
|
+
};
|
|
1273
|
+
const sessionIdentity = {
|
|
1274
|
+
googleSub: normalizeGoogleSubject(session?.sub),
|
|
1275
|
+
email: normalizeEmail(session?.email),
|
|
1276
|
+
ownerJid: normalizeJid(session?.ownerJid || ''),
|
|
1277
|
+
};
|
|
1278
|
+
const credential = sessionIdentity.googleSub || sessionIdentity.email || sessionIdentity.ownerJid ? await userPasswordAuthService.findCredentialByIdentity(sessionIdentity, { includeRevoked: true }).catch(() => null) : null;
|
|
1279
|
+
const passwordState = toUserPasswordStatePayload(credential);
|
|
1280
|
+
const view = normalizeMyProfileView(url?.searchParams?.get('view'));
|
|
1281
|
+
const shouldIncludePacks = view !== 'summary';
|
|
1282
|
+
|
|
1283
|
+
if (!session?.ownerJid && !session?.email && !session?.ownerPhone) {
|
|
1284
|
+
sendJson(req, res, 200, {
|
|
1285
|
+
data: {
|
|
1286
|
+
auth: { google: authGoogle },
|
|
1287
|
+
session: mapGoogleSessionResponseData(null),
|
|
1288
|
+
owner_jid: null,
|
|
1289
|
+
owner_jids: [],
|
|
1290
|
+
account: null,
|
|
1291
|
+
password: passwordState,
|
|
1292
|
+
packs: [],
|
|
1293
|
+
stats: shouldIncludePacks ? buildMyProfileStatsTemplate() : null,
|
|
1294
|
+
meta: {
|
|
1295
|
+
view,
|
|
1296
|
+
lazy: !shouldIncludePacks,
|
|
1297
|
+
},
|
|
1298
|
+
},
|
|
1299
|
+
});
|
|
1300
|
+
return;
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
const account = await resolveMyProfileAccountSummary(session);
|
|
1304
|
+
const ownerCandidates = await resolveMyProfileOwnerCandidates(session);
|
|
1305
|
+
const primaryOwnerJid = normalizeJid(session?.ownerJid || '') || ownerCandidates[0] || null;
|
|
1306
|
+
|
|
1307
|
+
if (!ownerCandidates.length) {
|
|
1308
|
+
sendJson(req, res, 200, {
|
|
1309
|
+
data: {
|
|
1310
|
+
auth: { google: authGoogle },
|
|
1311
|
+
session: mapGoogleSessionResponseData(session),
|
|
1312
|
+
owner_jid: primaryOwnerJid,
|
|
1313
|
+
owner_jids: [],
|
|
1314
|
+
account,
|
|
1315
|
+
password: passwordState,
|
|
1316
|
+
packs: [],
|
|
1317
|
+
stats: shouldIncludePacks ? buildMyProfileStatsTemplate() : null,
|
|
1318
|
+
meta: {
|
|
1319
|
+
view,
|
|
1320
|
+
lazy: !shouldIncludePacks,
|
|
1321
|
+
},
|
|
1322
|
+
},
|
|
1323
|
+
});
|
|
1324
|
+
return;
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
if (!shouldIncludePacks) {
|
|
1328
|
+
sendJson(req, res, 200, {
|
|
1329
|
+
data: {
|
|
1330
|
+
auth: { google: authGoogle },
|
|
1331
|
+
session: mapGoogleSessionResponseData(session),
|
|
1332
|
+
owner_jid: primaryOwnerJid,
|
|
1333
|
+
owner_jids: ownerCandidates,
|
|
1334
|
+
account,
|
|
1335
|
+
password: passwordState,
|
|
1336
|
+
packs: [],
|
|
1337
|
+
stats: null,
|
|
1338
|
+
meta: {
|
|
1339
|
+
view,
|
|
1340
|
+
lazy: true,
|
|
1341
|
+
},
|
|
1342
|
+
},
|
|
1343
|
+
});
|
|
1344
|
+
return;
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
const packLimit = clampInt(url?.searchParams?.get('limit'), view === 'packs' ? 120 : 300, 1, 300);
|
|
1348
|
+
const ownerPacks = await Promise.all(ownerCandidates.map((ownerJid) => listStickerPacksByOwner(ownerJid, { limit: 200, offset: 0 })));
|
|
1349
|
+
const includeAutoPacks = parseEnvBool(url?.searchParams?.get('include_auto'), parseEnvBool(process.env.STICKER_WEB_MY_PROFILE_INCLUDE_AUTO_PACKS, false));
|
|
1350
|
+
|
|
1351
|
+
const dedupPacks = new Map();
|
|
1352
|
+
for (const packList of ownerPacks) {
|
|
1353
|
+
for (const pack of Array.isArray(packList) ? packList : []) {
|
|
1354
|
+
if (!pack?.id) continue;
|
|
1355
|
+
if (shouldHidePackFromMyProfileDefault(pack, { includeAutoPacks })) continue;
|
|
1356
|
+
const existing = dedupPacks.get(pack.id);
|
|
1357
|
+
if (!existing) {
|
|
1358
|
+
dedupPacks.set(pack.id, pack);
|
|
1359
|
+
continue;
|
|
1360
|
+
}
|
|
1361
|
+
const currentUpdatedAt = Date.parse(String(pack.updated_at || pack.created_at || ''));
|
|
1362
|
+
const existingUpdatedAt = Date.parse(String(existing.updated_at || existing.created_at || ''));
|
|
1363
|
+
if (Number.isFinite(currentUpdatedAt) && (!Number.isFinite(existingUpdatedAt) || currentUpdatedAt > existingUpdatedAt)) {
|
|
1364
|
+
dedupPacks.set(pack.id, pack);
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
const packs = Array.from(dedupPacks.values())
|
|
1370
|
+
.sort((a, b) => {
|
|
1371
|
+
const aUpdatedAt = Date.parse(String(a?.updated_at || a?.created_at || ''));
|
|
1372
|
+
const bUpdatedAt = Date.parse(String(b?.updated_at || b?.created_at || ''));
|
|
1373
|
+
if (!Number.isFinite(aUpdatedAt) && !Number.isFinite(bUpdatedAt)) return 0;
|
|
1374
|
+
if (!Number.isFinite(aUpdatedAt)) return 1;
|
|
1375
|
+
if (!Number.isFinite(bUpdatedAt)) return -1;
|
|
1376
|
+
return bUpdatedAt - aUpdatedAt;
|
|
1377
|
+
})
|
|
1378
|
+
.slice(0, packLimit);
|
|
1379
|
+
|
|
1380
|
+
const engagementByPackId = await listStickerPackEngagementByPackIds(packs.map((pack) => pack.id));
|
|
1381
|
+
|
|
1382
|
+
const mappedPacks = packs.map((pack) => {
|
|
1383
|
+
const safeSummary = mapPackSummary(pack, engagementByPackId.get(pack.id) || null, null);
|
|
1384
|
+
const publicVisible = isPackPubliclyVisible(pack);
|
|
1385
|
+
return {
|
|
1386
|
+
...safeSummary,
|
|
1387
|
+
is_publicly_visible: publicVisible,
|
|
1388
|
+
cover_url: publicVisible ? safeSummary.cover_url : null,
|
|
1389
|
+
cover_preview_url: publicVisible ? safeSummary.cover_preview_url : null,
|
|
1390
|
+
};
|
|
1391
|
+
});
|
|
1392
|
+
|
|
1393
|
+
const stats = mappedPacks.reduce((acc, pack) => {
|
|
1394
|
+
acc.total += 1;
|
|
1395
|
+
const status = String(pack.status || '').toLowerCase();
|
|
1396
|
+
const visibility = String(pack.visibility || '').toLowerCase();
|
|
1397
|
+
if (status === 'published') acc.published += 1;
|
|
1398
|
+
if (status === 'draft') acc.drafts += 1;
|
|
1399
|
+
if (visibility === 'private') acc.private += 1;
|
|
1400
|
+
if (visibility === 'unlisted') acc.unlisted += 1;
|
|
1401
|
+
if (visibility === 'public') acc.public += 1;
|
|
1402
|
+
return acc;
|
|
1403
|
+
}, buildMyProfileStatsTemplate());
|
|
1404
|
+
|
|
1405
|
+
sendJson(req, res, 200, {
|
|
1406
|
+
data: {
|
|
1407
|
+
auth: { google: authGoogle },
|
|
1408
|
+
session: mapGoogleSessionResponseData(session),
|
|
1409
|
+
owner_jid: primaryOwnerJid,
|
|
1410
|
+
owner_jids: ownerCandidates,
|
|
1411
|
+
account,
|
|
1412
|
+
password: passwordState,
|
|
1413
|
+
packs: mappedPacks,
|
|
1414
|
+
stats,
|
|
1415
|
+
meta: {
|
|
1416
|
+
view,
|
|
1417
|
+
lazy: view === 'packs',
|
|
1418
|
+
},
|
|
1419
|
+
},
|
|
1420
|
+
});
|
|
1421
|
+
};
|
|
1422
|
+
|
|
1423
|
+
return {
|
|
1424
|
+
handlePasswordAuthRequest,
|
|
1425
|
+
handlePasswordRecoveryRequest,
|
|
1426
|
+
handlePasswordRecoveryVerifyRequest,
|
|
1427
|
+
handlePasswordRecoverySessionCreateRequest,
|
|
1428
|
+
handlePasswordRecoverySessionStatusRequest,
|
|
1429
|
+
handlePasswordRecoverySessionRequest,
|
|
1430
|
+
handlePasswordRecoverySessionVerifyRequest,
|
|
1431
|
+
handlePasswordLoginRequest,
|
|
1432
|
+
handleMyProfileRequest,
|
|
1433
|
+
};
|
|
1434
|
+
};
|