adserver-dashboard 1.0.0
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/.ci/staging.yml +191 -0
- package/.dockerignore +117 -0
- package/.env +40 -0
- package/.env.staging +38 -0
- package/.gitlab-ci.yml +16 -0
- package/DEMO_STATUS.md +579 -0
- package/Dockerfile +61 -0
- package/Influence-MW-AdServer-12-02-2026/client/index.html +17 -0
- package/Influence-MW-AdServer-12-02-2026/client/public/favicon.png +0 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/App.tsx +91 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/advanced-map-drawer.tsx +1131 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/ai-recommendation-panel.tsx +379 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/app-sidebar.tsx +183 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/auto-optimize-button.tsx +184 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/availability-drawer.tsx +385 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/brand-insights-panel.tsx +87 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/create-agency-drawer.tsx +198 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/create-brand-drawer.tsx +275 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/creative-assignment.tsx +526 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/data-table-toolbar.tsx +148 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/data-table.tsx +158 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/filter-drawer.tsx +356 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/form-insights-panel.tsx +82 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/geography-selector.tsx +699 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/header-user-menu.tsx +178 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/history-drawer.tsx +313 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/inventory-availability-section.tsx +176 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/inventory-format-drawer.tsx +173 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/inventory-selector.tsx +401 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/manual-inventory-drawer.tsx +368 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/mapbox-map.tsx +368 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/market-insights-panel.tsx +202 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/media-owner-drawer.tsx +217 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/metric-card.tsx +58 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/page-header.tsx +27 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/player-status-indicator.tsx +137 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/poi-targeting-drawer.tsx +298 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/recommendation-score-badge.tsx +102 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/recommended-inventories-panel.tsx +248 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/searchable-combobox.tsx +134 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/signal-visualizations.tsx +407 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/status-badge.tsx +35 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/theme-provider.tsx +73 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/theme-toggle.tsx +37 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/traffic-slider.tsx +75 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/accordion.tsx +56 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/alert-dialog.tsx +139 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/alert.tsx +59 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/aspect-ratio.tsx +5 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/avatar.tsx +51 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/badge.tsx +38 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/breadcrumb.tsx +115 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/button.tsx +62 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/calendar.tsx +68 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/card.tsx +85 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/carousel.tsx +260 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/chart.tsx +365 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/checkbox.tsx +28 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/collapsible.tsx +11 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/command.tsx +151 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/context-menu.tsx +198 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/dialog.tsx +122 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/drawer.tsx +118 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/dropdown-menu.tsx +198 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/form.tsx +178 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/hover-card.tsx +29 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/input-otp.tsx +69 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/input.tsx +23 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/label.tsx +24 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/menubar.tsx +256 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/navigation-menu.tsx +128 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/pagination.tsx +117 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/popover.tsx +29 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/progress.tsx +28 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/radio-group.tsx +42 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/resizable.tsx +45 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/scroll-area.tsx +46 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/select.tsx +160 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/separator.tsx +29 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/sheet.tsx +140 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/sidebar.tsx +727 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/skeleton.tsx +15 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/slider.tsx +26 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/switch.tsx +27 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/table.tsx +117 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/tabs.tsx +53 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/textarea.tsx +22 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/toast.tsx +127 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/toaster.tsx +33 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/toggle-group.tsx +61 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/toggle.tsx +43 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/tooltip.tsx +30 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/vendor-stores-modal.tsx +336 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/venue-type-drawer.tsx +359 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/venue-type-selector.tsx +436 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/hooks/use-mobile.tsx +19 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/hooks/use-toast.ts +191 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/index.css +244 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/lib/queryClient.ts +57 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/lib/utils.ts +39 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/lib/venue-taxonomy.ts +532 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/main.tsx +5 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/pages/assign-creative.tsx +781 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/pages/content-hub.tsx +995 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/pages/custom-pois.tsx +431 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/pages/dashboard.tsx +620 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/pages/deal-detail.tsx +1062 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/pages/deal-form.tsx +1570 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/pages/deals.tsx +716 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/pages/edit-creative-assignment.tsx +1051 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/pages/geotargeting.tsx +675 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/pages/integrations.tsx +425 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/pages/line-item-creatives.tsx +622 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/pages/line-item-form.tsx +3132 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/pages/line-items.tsx +530 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/pages/not-found.tsx +21 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/pages/proof-of-play-upload.tsx +479 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/pages/proof-of-play.tsx +880 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/pages/reports.tsx +235 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/pages/settings.tsx +652 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/pages/signal-form.tsx +1117 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/pages/signals.tsx +366 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/pages/tags.tsx +332 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/pages/venues.tsx +381 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/types/mapbox-gl-draw.d.ts +37 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/types/react-simple-maps.d.ts +57 -0
- package/Influence-MW-AdServer-12-02-2026/components.json +20 -0
- package/Influence-MW-AdServer-12-02-2026/docs/PRD.md +3373 -0
- package/Influence-MW-AdServer-12-02-2026/docs/influence-feature-mapping.csv +498 -0
- package/Influence-MW-AdServer-12-02-2026/drizzle.config.ts +14 -0
- package/Influence-MW-AdServer-12-02-2026/package-lock.json +9672 -0
- package/Influence-MW-AdServer-12-02-2026/package.json +118 -0
- package/Influence-MW-AdServer-12-02-2026/postcss.config.js +6 -0
- package/Influence-MW-AdServer-12-02-2026/replit.md +91 -0
- package/Influence-MW-AdServer-12-02-2026/script/build.ts +67 -0
- package/Influence-MW-AdServer-12-02-2026/scripts/create-miro-diagrams.cjs +318 -0
- package/Influence-MW-AdServer-12-02-2026/scripts/create-remaining-diagrams.cjs +270 -0
- package/Influence-MW-AdServer-12-02-2026/server/index.ts +103 -0
- package/Influence-MW-AdServer-12-02-2026/server/recommendation-service.ts +319 -0
- package/Influence-MW-AdServer-12-02-2026/server/routes.ts +1890 -0
- package/Influence-MW-AdServer-12-02-2026/server/static.ts +19 -0
- package/Influence-MW-AdServer-12-02-2026/server/storage.ts +2058 -0
- package/Influence-MW-AdServer-12-02-2026/server/vite.ts +58 -0
- package/Influence-MW-AdServer-12-02-2026/shared/schema.ts +1595 -0
- package/Influence-MW-AdServer-12-02-2026/tailwind.config.ts +107 -0
- package/Influence-MW-AdServer-12-02-2026/tsconfig.json +23 -0
- package/Influence-MW-AdServer-12-02-2026/vite.config.ts +40 -0
- package/LINE_ITEM_BUDGET_FIELD_MAPPING.md +178 -0
- package/PCM/.env.example +92 -0
- package/PCM/README.md +558 -0
- package/PCM/docs/TEST_CASES.md +422 -0
- package/PCM/index.js +106 -0
- package/PCM/package-lock.json +3282 -0
- package/PCM/package.json +32 -0
- package/PCM/replit.md +64 -0
- package/PCM/schema.sql +495 -0
- package/PCM/scripts/export-schema.js +183 -0
- package/PCM/scripts/seed-comprehensive.js +631 -0
- package/PCM/scripts/seed-production.js +477 -0
- package/PCM/src/config/db.js +56 -0
- package/PCM/src/config/swagger.js +5975 -0
- package/PCM/src/dto/EmailRequestDTO.js +166 -0
- package/PCM/src/middleware/errorHandler.js +52 -0
- package/PCM/src/middleware/logger.js +26 -0
- package/PCM/src/migrations/001_add_campaign_mode_fields.sql +36 -0
- package/PCM/src/migrations/002_create_deal_id_counters.sql +22 -0
- package/PCM/src/migrations/003_update_publishers_column.sql +15 -0
- package/PCM/src/migrations/004_add_direct_dealtype_and_advertiser.sql +5 -0
- package/PCM/src/migrations/005_add_programmatic_fields_and_update_enums.sql +31 -0
- package/PCM/src/migrations/006_add_line_item_programmatic_fields.sql +12 -0
- package/PCM/src/migrations/007_add_line_item_direct_fields.sql +15 -0
- package/PCM/src/migrations/008_add_inventory_fields.sql +45 -0
- package/PCM/src/migrations/009_move_inventory_fields_to_metadata.sql +32 -0
- package/PCM/src/migrations/010_add_draft_status_and_line_items_count.sql +23 -0
- package/PCM/src/migrations/011_add_planning_field.sql +21 -0
- package/PCM/src/migrations/012_fix_inventory_composite_pk.sql +17 -0
- package/PCM/src/migrations/013_make_external_id_optional.sql +3 -0
- package/PCM/src/migrations/014_create_change_history.sql +38 -0
- package/PCM/src/migrations/016_create_publisher_insertion_orders.sql +33 -0
- package/PCM/src/migrations/017_fix_line_item_id_fk_reference.sql +86 -0
- package/PCM/src/migrations/018_create_approval_tables.sql +44 -0
- package/PCM/src/migrations/019_add_encrypted_token_column.sql +2 -0
- package/PCM/src/migrations/020_add_rejection_reason_to_deals.sql +10 -0
- package/PCM/src/migrations/021_add_publisher_external_id_to_inventories.sql +12 -0
- package/PCM/src/migrations/022_add_line_item_extended_fields.sql +24 -0
- package/PCM/src/migrations/023_add_base_price_fields.sql +8 -0
- package/PCM/src/migrations/run-migrations.js +46 -0
- package/PCM/src/models/ApprovalOTP.js +51 -0
- package/PCM/src/models/ApprovalToken.js +79 -0
- package/PCM/src/models/ChangeHistory.js +107 -0
- package/PCM/src/models/Deal.js +186 -0
- package/PCM/src/models/DealIdCounter.js +28 -0
- package/PCM/src/models/LineItem.js +227 -0
- package/PCM/src/models/LineItemCreative.js +89 -0
- package/PCM/src/models/LineItemInventory.js +115 -0
- package/PCM/src/models/PublisherInsertionOrder.js +93 -0
- package/PCM/src/models/TransactionHistory.js +34 -0
- package/PCM/src/models/associations.js +81 -0
- package/PCM/src/routes/approval.js +321 -0
- package/PCM/src/routes/creatives.js +437 -0
- package/PCM/src/routes/deals.js +1638 -0
- package/PCM/src/routes/digitalSignage.js +242 -0
- package/PCM/src/routes/insertionOrders.js +380 -0
- package/PCM/src/routes/lineItems.js +926 -0
- package/PCM/src/routes/system.js +384 -0
- package/PCM/src/services/ApprovalService.js +885 -0
- package/PCM/src/services/CampaignImportConverter.js +631 -0
- package/PCM/src/services/CampaignModeService.js +273 -0
- package/PCM/src/services/CampaignStatusService.js +395 -0
- package/PCM/src/services/ChangeHistoryService.js +316 -0
- package/PCM/src/services/DealIdService.js +94 -0
- package/PCM/src/services/DealResponseFormatter.js +90 -0
- package/PCM/src/services/EmailNotificationService.js +315 -0
- package/PCM/src/services/LineItemResponseFormatter.js +122 -0
- package/PCM/src/services/LineItemStatusService.js +380 -0
- package/PCM/src/tests/comprehensiveTestRunner.js +360 -0
- package/PCM/src/tests/comprehensiveTests.js +1277 -0
- package/PCM/src/tests/dealTypeUnitTests.js +1058 -0
- package/PCM/src/tests/testRunner.js +248 -0
- package/PCM/src/utils/caseConverter.js +92 -0
- package/PCM/src/utils/dealCalculations.js +206 -0
- package/PCM/src/utils/lineItemPayloadNormalizer.js +41 -0
- package/PCM/src/utils/payloadNormalizer.js +34 -0
- package/PCM/src/utils/sourceNormalizer.js +56 -0
- package/PCM/src/validators/creativeValidator.js +27 -0
- package/PCM/src/validators/dealValidator.js +203 -0
- package/PCM/src/validators/lineItemValidator.js +489 -0
- package/PCM/tests/approval-flows.test.js +238 -0
- package/PCM/tests/approval-workflow.test.js +291 -0
- package/PCM/tests/campaign-import-converter.test.js +543 -0
- package/PCM/tests/campaign-import-e2e.test.js +520 -0
- package/PCM/tests/campaign-status.test.js +539 -0
- package/PCM/tests/direct-publisher-split-reimport.test.js +460 -0
- package/PCM/tests/e2e/digital-signage.test.js +145 -0
- package/PCM/tests/e2e/search-filter-pagination.test.js +399 -0
- package/PCM/tests/e2e-comprehensive.test.js +3446 -0
- package/PCM/tests/edge-cases.test.js +340 -0
- package/PCM/tests/line-item-status.test.js +340 -0
- package/PCM/tests/seller-account-external-ids.test.js +877 -0
- package/PCM/tests/source-validation.test.js +324 -0
- package/PRD.md +3373 -0
- package/README.md +186 -0
- package/client/index.html +35 -0
- package/client/public/DEMO_STATUS.md +579 -0
- package/client/public/img/MW-logo-trans_1754045676555.png +0 -0
- package/client/public/locales/ar/approval.json +144 -0
- package/client/public/locales/ar/buyer.json +61 -0
- package/client/public/locales/ar/campaigns.json +1 -0
- package/client/public/locales/ar/common.json +218 -0
- package/client/public/locales/ar/contentHub.json +266 -0
- package/client/public/locales/ar/creatives.json +79 -0
- package/client/public/locales/ar/dashboard.json +57 -0
- package/client/public/locales/ar/deals.json +886 -0
- package/client/public/locales/ar/dsp.json +131 -0
- package/client/public/locales/ar/inventory.json +201 -0
- package/client/public/locales/ar/lineItems.json +553 -0
- package/client/public/locales/ar/navigation.json +48 -0
- package/client/public/locales/ar/wizard.json +1 -0
- package/client/public/locales/en/approval.json +144 -0
- package/client/public/locales/en/buyer.json +65 -0
- package/client/public/locales/en/campaigns.json +1 -0
- package/client/public/locales/en/common.json +218 -0
- package/client/public/locales/en/contentHub.json +266 -0
- package/client/public/locales/en/creatives.json +79 -0
- package/client/public/locales/en/dashboard.json +57 -0
- package/client/public/locales/en/deals.json +886 -0
- package/client/public/locales/en/dsp.json +131 -0
- package/client/public/locales/en/inventory.json +201 -0
- package/client/public/locales/en/lineItems.json +659 -0
- package/client/public/locales/en/navigation.json +48 -0
- package/client/public/locales/en/wizard.json +1 -0
- package/client/public/locales/ja/approval.json +144 -0
- package/client/public/locales/ja/buyer.json +61 -0
- package/client/public/locales/ja/campaigns.json +1 -0
- package/client/public/locales/ja/common.json +218 -0
- package/client/public/locales/ja/contentHub.json +266 -0
- package/client/public/locales/ja/creatives.json +79 -0
- package/client/public/locales/ja/dashboard.json +57 -0
- package/client/public/locales/ja/deals.json +886 -0
- package/client/public/locales/ja/dsp.json +131 -0
- package/client/public/locales/ja/inventory.json +201 -0
- package/client/public/locales/ja/lineItems.json +553 -0
- package/client/public/locales/ja/navigation.json +48 -0
- package/client/public/locales/ja/wizard.json +1 -0
- package/client/public/locales/zh/approval.json +144 -0
- package/client/public/locales/zh/buyer.json +61 -0
- package/client/public/locales/zh/campaigns.json +1 -0
- package/client/public/locales/zh/common.json +218 -0
- package/client/public/locales/zh/contentHub.json +266 -0
- package/client/public/locales/zh/creatives.json +79 -0
- package/client/public/locales/zh/dashboard.json +57 -0
- package/client/public/locales/zh/deals.json +886 -0
- package/client/public/locales/zh/dsp.json +131 -0
- package/client/public/locales/zh/inventory.json +201 -0
- package/client/public/locales/zh/lineItems.json +553 -0
- package/client/public/locales/zh/navigation.json +48 -0
- package/client/public/locales/zh/wizard.json +1 -0
- package/client/public/manifest.json +36 -0
- package/client/src/App.tsx +464 -0
- package/client/src/components/app-sidebar.tsx +312 -0
- package/client/src/components/approval/approval-decision-form.test.tsx +294 -0
- package/client/src/components/approval/approval-decision-form.tsx +326 -0
- package/client/src/components/approval/approval-sheet.tsx +631 -0
- package/client/src/components/approval/line-item-details-sheet.tsx +371 -0
- package/client/src/components/approval/otp-verification.test.tsx +337 -0
- package/client/src/components/approval/otp-verification.tsx +180 -0
- package/client/src/components/content-hub/bulk-transcode-dialog.tsx +379 -0
- package/client/src/components/content-hub/content-hub-manager-v2.tsx +574 -0
- package/client/src/components/content-hub/content-hub-manager.tsx +330 -0
- package/client/src/components/content-hub/creative-card.tsx +456 -0
- package/client/src/components/content-hub/creative-detail-sheet.tsx +685 -0
- package/client/src/components/content-hub/creative-filters.tsx +457 -0
- package/client/src/components/content-hub/creative-grid.tsx +329 -0
- package/client/src/components/content-hub/creative-selector.tsx +415 -0
- package/client/src/components/content-hub/creative-upload.tsx +547 -0
- package/client/src/components/content-hub/folder-dialogs.tsx +445 -0
- package/client/src/components/content-hub/folder-list.tsx +280 -0
- package/client/src/components/content-hub/review-dialogs.tsx +268 -0
- package/client/src/components/content-hub/transcode-dialog.tsx +226 -0
- package/client/src/components/creative-library/creative-details-view.tsx +446 -0
- package/client/src/components/creative-library/creative-filters-panel.tsx +203 -0
- package/client/src/components/creative-library/creative-list.tsx +360 -0
- package/client/src/components/creative-library/creative-status-badge.tsx +71 -0
- package/client/src/components/creative-library/folder-card.tsx +78 -0
- package/client/src/components/creative-library/index.ts +27 -0
- package/client/src/components/creative-library/new-creative-card.tsx +211 -0
- package/client/src/components/creative-library/upload-creative-dialog.tsx +261 -0
- package/client/src/components/dashboard-overview.tsx +109 -0
- package/client/src/components/deals/approval-history-panel.test.tsx +240 -0
- package/client/src/components/deals/approval-history-panel.tsx +156 -0
- package/client/src/components/deals/deal-status-badge.tsx +92 -0
- package/client/src/components/deals/import-from-planner-dialog.tsx +399 -0
- package/client/src/components/deals/market-insights-panel.tsx +237 -0
- package/client/src/components/deals/reopen-deal-sheet.tsx +191 -0
- package/client/src/components/deals/request-approval-sheet.test.tsx +323 -0
- package/client/src/components/deals/request-approval-sheet.tsx +136 -0
- package/client/src/components/deals/resend-approval-sheet.tsx +201 -0
- package/client/src/components/direct-campaigns/campaign-card.tsx +283 -0
- package/client/src/components/direct-campaigns/deal-filter-panel.tsx +325 -0
- package/client/src/components/inventory/advanced-filters-panel.tsx +273 -0
- package/client/src/components/inventory/csv-upload-modal.tsx +639 -0
- package/client/src/components/inventory/inventory-availability-view.tsx +486 -0
- package/client/src/components/inventory/inventory-details-sheet.tsx +376 -0
- package/client/src/components/inventory/inventory-map-view.tsx +596 -0
- package/client/src/components/inventory/inventory-settings-menu.tsx +52 -0
- package/client/src/components/language-switcher.tsx +53 -0
- package/client/src/components/line-items/campaign-forecast-panel.tsx +138 -0
- package/client/src/components/line-items/form-insights.tsx +89 -0
- package/client/src/components/line-items/geofencing/LocationCsvUploadDrawer.tsx +100 -0
- package/client/src/components/line-items/geofencing/POIDropdown.tsx +379 -0
- package/client/src/components/line-items/geofencing/SelectedLocationsSidebar.tsx +436 -0
- package/client/src/components/line-items/geofencing/ViewFileLocationDrawer.tsx +199 -0
- package/client/src/components/line-items/geofencing/components/ExistingFilesTab.tsx +268 -0
- package/client/src/components/line-items/geofencing/components/TemplateDownloadSection.tsx +59 -0
- package/client/src/components/line-items/geofencing/components/UploadTab.tsx +215 -0
- package/client/src/components/line-items/geofencing-map.tsx +1270 -0
- package/client/src/components/line-items/inventory-availability-section.tsx +178 -0
- package/client/src/components/line-items/line-item-schedule-manager.tsx +313 -0
- package/client/src/components/line-items/manual-inventory-drawer.tsx +346 -0
- package/client/src/components/line-items/planner-inventory-card.tsx +495 -0
- package/client/src/components/line-items/planner-schedule-grid.tsx +495 -0
- package/client/src/components/line-items/schedule-rule-editor.tsx +649 -0
- package/client/src/components/line-items/schedule-rule-types.ts +122 -0
- package/client/src/components/line-items/steps/creatives-step.tsx +681 -0
- package/client/src/components/line-items/steps/inventory-schedule-step.tsx +1596 -0
- package/client/src/components/line-items/steps/inventory-step.tsx +1533 -0
- package/client/src/components/line-items/steps/line-item-details-step.tsx +916 -0
- package/client/src/components/line-items/steps/schedule-step.tsx +273 -0
- package/client/src/components/line-items/steps/summary-step.tsx +680 -0
- package/client/src/components/line-items/steps/targeting-step.tsx +1708 -0
- package/client/src/components/product-switcher.tsx +105 -0
- package/client/src/components/protected-route.tsx +49 -0
- package/client/src/components/skip-link.tsx +22 -0
- package/client/src/components/stat-card.tsx +53 -0
- package/client/src/components/status-badge.tsx +96 -0
- package/client/src/components/ui/hierarchical-venue-selector.tsx +389 -0
- package/client/src/components/ui/toaster.tsx +111 -0
- package/client/src/contexts/auth-context.tsx +181 -0
- package/client/src/contexts/sidebar-state.tsx +50 -0
- package/client/src/contexts/theme-context.tsx +66 -0
- package/client/src/data/campaign-data.json +107 -0
- package/client/src/data/countries.json +22 -0
- package/client/src/hooks/use-approval.ts +366 -0
- package/client/src/hooks/use-keyboard-shortcuts.ts +74 -0
- package/client/src/hooks/use-media-query.ts +46 -0
- package/client/src/hooks/use-mobile.tsx +19 -0
- package/client/src/hooks/use-page-title.ts +21 -0
- package/client/src/hooks/use-toast.ts +195 -0
- package/client/src/index.css +694 -0
- package/client/src/lib/__tests__/accessibility.test.ts +104 -0
- package/client/src/lib/__tests__/date-utils.test.ts +199 -0
- package/client/src/lib/__tests__/dsp-buyer-api.test.ts +127 -0
- package/client/src/lib/__tests__/dsp-buyer-integration.test.ts +247 -0
- package/client/src/lib/__tests__/storage-utils.test.ts +167 -0
- package/client/src/lib/__tests__/utils.test.ts +57 -0
- package/client/src/lib/accessibility.ts +141 -0
- package/client/src/lib/api-config.ts +9 -0
- package/client/src/lib/auth-service.ts +209 -0
- package/client/src/lib/campaign-creative-api.ts +82 -0
- package/client/src/lib/company-api.ts +61 -0
- package/client/src/lib/content-hub-api.ts +407 -0
- package/client/src/lib/creative-mapper.ts +61 -0
- package/client/src/lib/date-utils.ts +119 -0
- package/client/src/lib/deal-helpers.ts +220 -0
- package/client/src/lib/dsp-buyer-api.ts +196 -0
- package/client/src/lib/geo-import-api.ts +151 -0
- package/client/src/lib/google-poi-api.ts +305 -0
- package/client/src/lib/i18n/__tests__/formatting.test.ts +202 -0
- package/client/src/lib/i18n/formatting.ts +130 -0
- package/client/src/lib/i18n/index.ts +8 -0
- package/client/src/lib/i18n-compat.ts +76 -0
- package/client/src/lib/influence-deals-api.ts +896 -0
- package/client/src/lib/inventory-api.ts +399 -0
- package/client/src/lib/oauth-service.ts +678 -0
- package/client/src/lib/poi-types.ts +75 -0
- package/client/src/lib/queryClient.ts +144 -0
- package/client/src/lib/recommendation-api.ts +380 -0
- package/client/src/lib/storage-utils.ts +104 -0
- package/client/src/lib/tolgee.ts +85 -0
- package/client/src/lib/utils.ts +0 -0
- package/client/src/main.tsx +67 -0
- package/client/src/mapbox-draw-modes.d.ts +32 -0
- package/client/src/pages/all-folders.tsx +203 -0
- package/client/src/pages/auth-callback.tsx +115 -0
- package/client/src/pages/buyer-form.tsx +339 -0
- package/client/src/pages/buyer-list.tsx +622 -0
- package/client/src/pages/content-hub.tsx +1358 -0
- package/client/src/pages/create-deal.tsx +2093 -0
- package/client/src/pages/creative-assignment-page.tsx +548 -0
- package/client/src/pages/creatives.tsx +5 -0
- package/client/src/pages/custom-pois.tsx +425 -0
- package/client/src/pages/dashboard.tsx +615 -0
- package/client/src/pages/deal-history.tsx +434 -0
- package/client/src/pages/deal-line-items.tsx +1703 -0
- package/client/src/pages/demo-status.tsx +113 -0
- package/client/src/pages/direct-campaign-details.tsx +361 -0
- package/client/src/pages/direct-campaigns-new.tsx +824 -0
- package/client/src/pages/dsp-form.tsx +803 -0
- package/client/src/pages/dsp-list.tsx +239 -0
- package/client/src/pages/folder-content.tsx +336 -0
- package/client/src/pages/integrations.tsx +429 -0
- package/client/src/pages/line-item-creatives.tsx +789 -0
- package/client/src/pages/line-item-detail-page.tsx +684 -0
- package/client/src/pages/line-item-form-page.tsx +3261 -0
- package/client/src/pages/line-item-wizard.tsx +1207 -0
- package/client/src/pages/login.tsx +154 -0
- package/client/src/pages/not-found.tsx +23 -0
- package/client/src/pages/proof-of-play.tsx +397 -0
- package/client/src/pages/public-approval.tsx +551 -0
- package/client/src/pages/reports.tsx +231 -0
- package/client/src/pages/settings.tsx +760 -0
- package/client/src/pages/signals.tsx +389 -0
- package/client/src/pages/tags.tsx +318 -0
- package/client/src/pages/test-results.tsx +328 -0
- package/client/src/store/hooks.ts +5 -0
- package/client/src/store/index.ts +15 -0
- package/client/src/store/mapMarkerLocationsSlice.ts +241 -0
- package/client/src/styles/design-tokens.css +324 -0
- package/client/src/test/setup.ts +261 -0
- package/client/src/test/test-utils.tsx +40 -0
- package/client/src/types/approval.ts +221 -0
- package/client/src/types/content-hub.ts +209 -0
- package/client/src/types/geofencing.ts +67 -0
- package/client/src/types/transcoding.ts +140 -0
- package/client/src/vite-env.d.ts +18 -0
- package/components.json +20 -0
- package/creative-api.json +1 -0
- package/docs/AI_REFERENCE.md +459 -0
- package/docs/MWDesign-Prompt.md +132 -0
- package/docs/MWDesign-System.md +344 -0
- package/docs/test-plan.md +277 -0
- package/e2e/AUTONOMOUS-TESTING.md +406 -0
- package/e2e/README.md +219 -0
- package/e2e/autonomous-flow.spec.ts +308 -0
- package/e2e/debug-sso.spec.ts +163 -0
- package/e2e/direct-campaigns.spec.ts +219 -0
- package/e2e/explore-sso.spec.ts +149 -0
- package/e2e/fixtures/auth.ts +26 -0
- package/e2e/fixtures/enhanced-test.ts +331 -0
- package/e2e/pagination.spec.ts +280 -0
- package/e2e/view-toggle.spec.ts +312 -0
- package/generated-icon.png +0 -0
- package/i18next-scanner.config.cjs +46 -0
- package/package.json +141 -0
- package/playwright.config.ts +93 -0
- package/postcss.config.js +6 -0
- package/replit.md +196 -0
- package/screenshot-after-login.png +0 -0
- package/screenshot-contenthub-grid.png +0 -0
- package/screenshot-contenthub-list-fixed.png +0 -0
- package/screenshot-contenthub-list.png +0 -0
- package/screenshot-create-deal.png +0 -0
- package/screenshot-dashboard.png +0 -0
- package/screenshot-deals.png +0 -0
- package/screenshot-login-filled.png +0 -0
- package/screenshot-login.png +0 -0
- package/screenshot.mjs +24 -0
- package/scripts/deploy-stg.sh +185 -0
- package/shared/direct-io-schema.ts +383 -0
- package/shared/schema.ts +439 -0
- package/shared/screen-types.ts +149 -0
- package/springdocDefault.json +1 -0
- package/swagger-ui-bundle.js +2 -0
- package/swagger-ui-init.js +10316 -0
- package/tailwind.config.ts +282 -0
- package/terraform/README.md +306 -0
- package/terraform/cloudfront.tf +289 -0
- package/terraform/ecs.tf +727 -0
- package/terraform/environments/dev.tfvars +59 -0
- package/terraform/environments/production.tfvars +60 -0
- package/terraform/main.tf +47 -0
- package/terraform/outputs.tf +145 -0
- package/terraform/s3.tf +192 -0
- package/terraform/variables.tf +226 -0
- package/terraform/waf.tf +165 -0
- package/terraform-frontend/.terraform.lock.hcl +25 -0
- package/terraform-frontend/README.md +85 -0
- package/terraform-frontend/cloudfront.tf +125 -0
- package/terraform-frontend/main.tf +31 -0
- package/terraform-frontend/outputs.tf +24 -0
- package/terraform-frontend/terraform.tfvars +12 -0
- package/terraform-frontend/variables.tf +53 -0
- package/tsconfig.json +23 -0
- package/vite.config.ts +226 -0
- package/vitest.config.ts +56 -0
|
@@ -0,0 +1,678 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth Service with PKCE
|
|
3
|
+
* Handles OAuth 2.0 authorization code flow with PKCE using secure cookies
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { setItem, getItem, removeItem, clearAll } from "./storage-utils";
|
|
7
|
+
|
|
8
|
+
// OAuth Configuration
|
|
9
|
+
// Determine redirect URI dynamically based on current domain
|
|
10
|
+
function getRedirectUri(): string {
|
|
11
|
+
const origin = window.location.origin;
|
|
12
|
+
|
|
13
|
+
// If running on Replit domains (.replit.dev for dev, .replit.app for production), use the current origin
|
|
14
|
+
if (origin.includes(".replit.dev") || origin.includes(".replit.app")) {
|
|
15
|
+
console.log("[OAuth] Using dynamic redirect URI for Replit domain");
|
|
16
|
+
return `${origin}/auth/callback`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// For local development or other domains, use environment variable or fallback to current origin
|
|
20
|
+
return import.meta.env.VITE_OAUTH_REDIRECT_URI || `${origin}/auth/callback`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const OAUTH_CONFIG = {
|
|
24
|
+
clientId: import.meta.env.VITE_OAUTH_CLIENT_ID || "",
|
|
25
|
+
get redirectUri() {
|
|
26
|
+
return getRedirectUri();
|
|
27
|
+
},
|
|
28
|
+
apiBaseUrl: import.meta.env.VITE_OAUTH_API_URL,
|
|
29
|
+
scope: "profile email",
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// Storage keys
|
|
33
|
+
const STORAGE_KEYS = {
|
|
34
|
+
ACCESS_TOKEN: "access_token",
|
|
35
|
+
REFRESH_TOKEN: "refresh_token",
|
|
36
|
+
USER: "user",
|
|
37
|
+
TOKEN_EXPIRY: "token_expiry",
|
|
38
|
+
CODE_VERIFIER: "code_verifier",
|
|
39
|
+
STATE: "state",
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export interface OAuthTokens {
|
|
43
|
+
access_token: string;
|
|
44
|
+
refresh_token: string;
|
|
45
|
+
expires_in?: number;
|
|
46
|
+
token_type?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface UserProfile {
|
|
50
|
+
id: string;
|
|
51
|
+
email: string;
|
|
52
|
+
name?: string;
|
|
53
|
+
avatar_url?: string;
|
|
54
|
+
company_id?: string;
|
|
55
|
+
company_name?: string;
|
|
56
|
+
role_name?: string;
|
|
57
|
+
company_country?: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface CompanyAddress {
|
|
61
|
+
id: string;
|
|
62
|
+
company_id: string;
|
|
63
|
+
address_type: string;
|
|
64
|
+
is_primary: boolean;
|
|
65
|
+
label?: string;
|
|
66
|
+
street1?: string;
|
|
67
|
+
street2?: string;
|
|
68
|
+
city?: string;
|
|
69
|
+
state?: string;
|
|
70
|
+
postal_code?: string;
|
|
71
|
+
country?: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface CompanyBrand {
|
|
75
|
+
id: string;
|
|
76
|
+
name: string;
|
|
77
|
+
code: string;
|
|
78
|
+
description?: string;
|
|
79
|
+
logo_url?: string;
|
|
80
|
+
is_active: boolean;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface CompanyBrandsResponse {
|
|
84
|
+
brands: CompanyBrand[];
|
|
85
|
+
total: number;
|
|
86
|
+
page: number;
|
|
87
|
+
limit: number;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Generate a random string for PKCE code verifier or state
|
|
92
|
+
*/
|
|
93
|
+
function generateRandomString(length: number = 128): string {
|
|
94
|
+
const array = new Uint8Array(length);
|
|
95
|
+
crypto.getRandomValues(array);
|
|
96
|
+
let result = "";
|
|
97
|
+
for (let i = 0; i < array.length; i++) {
|
|
98
|
+
result += array[i].toString(16).padStart(2, "0");
|
|
99
|
+
}
|
|
100
|
+
return result.substring(0, length);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Generate PKCE code challenge from verifier
|
|
105
|
+
* Uses SHA-256 hash and base64url encoding
|
|
106
|
+
*/
|
|
107
|
+
async function generateCodeChallenge(verifier: string): Promise<string> {
|
|
108
|
+
const encoder = new TextEncoder();
|
|
109
|
+
const data = encoder.encode(verifier);
|
|
110
|
+
const hash = await crypto.subtle.digest("SHA-256", data);
|
|
111
|
+
|
|
112
|
+
// Convert to base64url
|
|
113
|
+
const hashArray = Array.from(new Uint8Array(hash));
|
|
114
|
+
return btoa(String.fromCharCode(...hashArray))
|
|
115
|
+
.replace(/\+/g, "-")
|
|
116
|
+
.replace(/\//g, "_")
|
|
117
|
+
.replace(/=/g, "");
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Initiate OAuth authorization flow
|
|
122
|
+
* Redirects user to Direct Campaigns authorization page
|
|
123
|
+
*/
|
|
124
|
+
export async function authorize(): Promise<void> {
|
|
125
|
+
// Generate random state for CSRF protection
|
|
126
|
+
const state = generateRandomString(32);
|
|
127
|
+
|
|
128
|
+
// Store state for later verification (optional - API may handle this)
|
|
129
|
+
setItem(STORAGE_KEYS.STATE, state);
|
|
130
|
+
|
|
131
|
+
const params = new URLSearchParams({
|
|
132
|
+
client_id: OAUTH_CONFIG.clientId,
|
|
133
|
+
redirect_uri: OAUTH_CONFIG.redirectUri,
|
|
134
|
+
response_type: "code",
|
|
135
|
+
scope: OAUTH_CONFIG.scope,
|
|
136
|
+
state,
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
const authUrl = `${OAUTH_CONFIG.apiBaseUrl}/api/v1/auth/authorize?${params.toString()}`;
|
|
140
|
+
console.log("[OAuth] Redirecting to:", authUrl);
|
|
141
|
+
window.location.href = authUrl;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Handle OAuth callback and exchange code for tokens
|
|
146
|
+
*/
|
|
147
|
+
export async function handleCallback(
|
|
148
|
+
code: string,
|
|
149
|
+
state: string,
|
|
150
|
+
): Promise<UserProfile> {
|
|
151
|
+
// Log callback received
|
|
152
|
+
console.log("[OAuth] Callback received with code and state");
|
|
153
|
+
|
|
154
|
+
// Clear state from storage (don't fail if mismatch - API may handle state differently)
|
|
155
|
+
const savedState = getItem(STORAGE_KEYS.STATE);
|
|
156
|
+
if (state && savedState && state !== savedState) {
|
|
157
|
+
console.warn(
|
|
158
|
+
"[OAuth] State mismatch but continuing (API may handle state internally)",
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
removeItem(STORAGE_KEYS.STATE);
|
|
162
|
+
|
|
163
|
+
// Exchange authorization code for tokens via GET request
|
|
164
|
+
// API requires both code and state parameters
|
|
165
|
+
const params = new URLSearchParams({
|
|
166
|
+
code,
|
|
167
|
+
state,
|
|
168
|
+
redirect_uri: OAUTH_CONFIG.redirectUri,
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
const callbackUrl = `${OAUTH_CONFIG.apiBaseUrl}/api/v1/auth/callback?${params.toString()}`;
|
|
172
|
+
console.log("[OAuth] Exchanging code at:", callbackUrl);
|
|
173
|
+
|
|
174
|
+
const response = await fetch(callbackUrl, {
|
|
175
|
+
method: "GET",
|
|
176
|
+
headers: {
|
|
177
|
+
accept: "application/json",
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
if (!response.ok) {
|
|
182
|
+
const error = await response
|
|
183
|
+
.json()
|
|
184
|
+
.catch(() => ({ message: `HTTP ${response.status}` }));
|
|
185
|
+
console.error("[OAuth] Token exchange failed:", error);
|
|
186
|
+
throw new Error(
|
|
187
|
+
error.message || error.error_description || "OAuth callback failed",
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const data = await response.json();
|
|
192
|
+
console.log("[OAuth] Token response received");
|
|
193
|
+
|
|
194
|
+
// Extract tokens from result object (handle both nested and flat responses)
|
|
195
|
+
const tokens = data.result || data;
|
|
196
|
+
|
|
197
|
+
if (!tokens.access_token) {
|
|
198
|
+
console.error("[OAuth] No access token in response:", tokens);
|
|
199
|
+
throw new Error("No access token in response");
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Store tokens in cookies
|
|
203
|
+
storeTokens(tokens.access_token, tokens.refresh_token, tokens.expires_in);
|
|
204
|
+
console.log("[OAuth] Tokens stored in cookies successfully");
|
|
205
|
+
|
|
206
|
+
// Try to fetch user profile
|
|
207
|
+
let user: UserProfile;
|
|
208
|
+
try {
|
|
209
|
+
user = await fetchUserProfile(tokens.access_token);
|
|
210
|
+
storeUser(user);
|
|
211
|
+
} catch (error) {
|
|
212
|
+
console.warn("[OAuth] Could not fetch user profile:", error);
|
|
213
|
+
// If profile fetch fails, try to extract user info from response
|
|
214
|
+
if (tokens.user) {
|
|
215
|
+
user = tokens.user;
|
|
216
|
+
} else {
|
|
217
|
+
// Fallback: create minimal user object if profile endpoint doesn't exist
|
|
218
|
+
console.warn("[OAuth] Using fallback minimal user object");
|
|
219
|
+
user = {
|
|
220
|
+
id: "user",
|
|
221
|
+
email: "user@example.com",
|
|
222
|
+
name: "User",
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
storeUser(user);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return user;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Store tokens in cookies
|
|
233
|
+
*/
|
|
234
|
+
export function storeTokens(
|
|
235
|
+
accessToken: string,
|
|
236
|
+
refreshToken: string,
|
|
237
|
+
expiresIn?: number,
|
|
238
|
+
): void {
|
|
239
|
+
const accessExpiry = expiresIn || 3600; // Default 1 hour
|
|
240
|
+
|
|
241
|
+
setItem(STORAGE_KEYS.ACCESS_TOKEN, accessToken);
|
|
242
|
+
setItem(STORAGE_KEYS.REFRESH_TOKEN, refreshToken);
|
|
243
|
+
|
|
244
|
+
// Store token expiry time (in milliseconds)
|
|
245
|
+
const expiryTime = Date.now() + accessExpiry * 1000;
|
|
246
|
+
setItem(STORAGE_KEYS.TOKEN_EXPIRY, expiryTime.toString());
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Store user profile in cookies
|
|
251
|
+
*/
|
|
252
|
+
export function storeUser(user: UserProfile): void {
|
|
253
|
+
setItem(STORAGE_KEYS.USER, JSON.stringify(user));
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Get access token from cookies
|
|
258
|
+
*/
|
|
259
|
+
export function getAccessToken(): string | null {
|
|
260
|
+
return getItem(STORAGE_KEYS.ACCESS_TOKEN);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Get refresh token from cookies
|
|
265
|
+
*/
|
|
266
|
+
export function getRefreshToken(): string | null {
|
|
267
|
+
return getItem(STORAGE_KEYS.REFRESH_TOKEN);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Get user profile from cookies
|
|
272
|
+
*/
|
|
273
|
+
export function getUser(): UserProfile | null {
|
|
274
|
+
const userJson = getItem(STORAGE_KEYS.USER);
|
|
275
|
+
if (!userJson) return null;
|
|
276
|
+
|
|
277
|
+
try {
|
|
278
|
+
return JSON.parse(userJson);
|
|
279
|
+
} catch (error) {
|
|
280
|
+
console.error("Failed to parse user data from cookies:", error);
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Check if user is authenticated (has valid tokens)
|
|
287
|
+
*/
|
|
288
|
+
export function isAuthenticated(): boolean {
|
|
289
|
+
const accessToken = getAccessToken();
|
|
290
|
+
const refreshToken = getRefreshToken();
|
|
291
|
+
return !!(accessToken || refreshToken);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Check if access token is expired
|
|
296
|
+
*/
|
|
297
|
+
export function isTokenExpired(): boolean {
|
|
298
|
+
const expiryStr = getItem(STORAGE_KEYS.TOKEN_EXPIRY);
|
|
299
|
+
if (!expiryStr) return true;
|
|
300
|
+
|
|
301
|
+
const expiry = parseInt(expiryStr, 10);
|
|
302
|
+
// Add 5 minute buffer before actual expiry
|
|
303
|
+
return Date.now() >= expiry - 300000;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Refresh access token using refresh token
|
|
308
|
+
* Uses POST /api/v1/auth/refresh endpoint
|
|
309
|
+
*/
|
|
310
|
+
export async function refreshAccessToken(): Promise<void> {
|
|
311
|
+
const refreshToken = getRefreshToken();
|
|
312
|
+
if (!refreshToken) {
|
|
313
|
+
throw new Error("No refresh token available");
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
console.log("[OAuth] Refreshing access token");
|
|
317
|
+
|
|
318
|
+
const response = await fetch(
|
|
319
|
+
`${OAUTH_CONFIG.apiBaseUrl}/api/v1/auth/refresh`,
|
|
320
|
+
{
|
|
321
|
+
method: "POST",
|
|
322
|
+
headers: {
|
|
323
|
+
accept: "application/json",
|
|
324
|
+
"Content-Type": "application/json",
|
|
325
|
+
},
|
|
326
|
+
body: JSON.stringify({
|
|
327
|
+
refresh_token: refreshToken,
|
|
328
|
+
}),
|
|
329
|
+
},
|
|
330
|
+
);
|
|
331
|
+
|
|
332
|
+
if (!response.ok) {
|
|
333
|
+
const errorData = await response
|
|
334
|
+
.json()
|
|
335
|
+
.catch(() => ({ message: `HTTP ${response.status}` }));
|
|
336
|
+
console.error("[OAuth] Token refresh failed:", errorData);
|
|
337
|
+
throw new Error(errorData.message || "Failed to refresh access token");
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const data = await response.json();
|
|
341
|
+
console.log("[OAuth] Token refresh successful");
|
|
342
|
+
|
|
343
|
+
// Extract tokens from result object (handle both nested and flat responses)
|
|
344
|
+
const tokens = data.result || data;
|
|
345
|
+
|
|
346
|
+
if (!tokens.access_token) {
|
|
347
|
+
console.error("[OAuth] No access token in refresh response:", tokens);
|
|
348
|
+
throw new Error("No access token in refresh response");
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Update tokens in cookies
|
|
352
|
+
storeTokens(tokens.access_token, tokens.refresh_token, tokens.expires_in);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Fetch user profile from API
|
|
357
|
+
*/
|
|
358
|
+
export async function fetchUserProfile(
|
|
359
|
+
accessToken?: string,
|
|
360
|
+
): Promise<UserProfile> {
|
|
361
|
+
const token = accessToken || getAccessToken();
|
|
362
|
+
if (!token) {
|
|
363
|
+
throw new Error("No access token available");
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
console.log(
|
|
367
|
+
"[OAuth] Fetching user profile from:",
|
|
368
|
+
`${OAUTH_CONFIG.apiBaseUrl}/api/v1/auth/userInfo`,
|
|
369
|
+
);
|
|
370
|
+
|
|
371
|
+
let response: Response;
|
|
372
|
+
try {
|
|
373
|
+
response = await fetch(`${OAUTH_CONFIG.apiBaseUrl}/api/v1/auth/userInfo`, {
|
|
374
|
+
method: "GET",
|
|
375
|
+
mode: "cors",
|
|
376
|
+
credentials: "omit",
|
|
377
|
+
headers: {
|
|
378
|
+
Authorization: `Bearer ${token}`,
|
|
379
|
+
Accept: "application/json",
|
|
380
|
+
},
|
|
381
|
+
});
|
|
382
|
+
} catch (networkError) {
|
|
383
|
+
console.error("[OAuth] Network error fetching user profile:", networkError);
|
|
384
|
+
throw new Error(
|
|
385
|
+
`Network error: ${networkError instanceof Error ? networkError.message : "Unknown error"}`,
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (!response.ok) {
|
|
390
|
+
const errorText = await response.text().catch(() => "");
|
|
391
|
+
console.error(
|
|
392
|
+
"[OAuth] User profile fetch failed:",
|
|
393
|
+
response.status,
|
|
394
|
+
errorText,
|
|
395
|
+
);
|
|
396
|
+
throw new Error(`Failed to fetch user profile: ${response.status}`);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const data = await response.json();
|
|
400
|
+
|
|
401
|
+
// Extract user profile from result object
|
|
402
|
+
const profileData = data.result || data;
|
|
403
|
+
|
|
404
|
+
// Map API response to UserProfile interface
|
|
405
|
+
// Construct full name from first_name and last_name if available
|
|
406
|
+
let fullName = profileData.username || "User";
|
|
407
|
+
if (profileData.first_name || profileData.last_name) {
|
|
408
|
+
fullName =
|
|
409
|
+
`${profileData.first_name || ""} ${profileData.last_name || ""}`.trim();
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Extract company and role from memberships
|
|
413
|
+
let companyId = "";
|
|
414
|
+
let companyName = "";
|
|
415
|
+
let roleName = "";
|
|
416
|
+
if (profileData.memberships && profileData.memberships.length > 0) {
|
|
417
|
+
const activeMembership =
|
|
418
|
+
profileData.memberships.find((m: any) => m.is_active) ||
|
|
419
|
+
profileData.memberships[0];
|
|
420
|
+
companyId = activeMembership.company_id || activeMembership.id || "";
|
|
421
|
+
companyName = activeMembership.company_name || "";
|
|
422
|
+
roleName = activeMembership.role_name || "";
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const userProfile: UserProfile = {
|
|
426
|
+
id: profileData.user_id || profileData.id || "user",
|
|
427
|
+
email: profileData.email || "",
|
|
428
|
+
name: fullName,
|
|
429
|
+
avatar_url: profileData.avatar_url,
|
|
430
|
+
company_id: companyId,
|
|
431
|
+
company_name: companyName,
|
|
432
|
+
role_name: roleName,
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
console.log("[OAuth] User profile mapped:", userProfile);
|
|
436
|
+
return userProfile;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Logout and clear all auth data
|
|
441
|
+
*/
|
|
442
|
+
export async function logout(): Promise<void> {
|
|
443
|
+
const refreshToken = getRefreshToken();
|
|
444
|
+
|
|
445
|
+
// Call logout endpoint with refresh token
|
|
446
|
+
if (refreshToken) {
|
|
447
|
+
try {
|
|
448
|
+
await fetch(`${OAUTH_CONFIG.apiBaseUrl}/api/v1/auth/logout`, {
|
|
449
|
+
method: "POST",
|
|
450
|
+
headers: {
|
|
451
|
+
"Content-Type": "application/json",
|
|
452
|
+
},
|
|
453
|
+
body: JSON.stringify({
|
|
454
|
+
refresh_token: refreshToken,
|
|
455
|
+
}),
|
|
456
|
+
});
|
|
457
|
+
} catch (error) {
|
|
458
|
+
console.error("Logout API call failed:", error);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Clear all OAuth data from localStorage
|
|
463
|
+
clearAuthStorage();
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Clear all OAuth data from cookies
|
|
468
|
+
*/
|
|
469
|
+
export function clearAuthStorage(): void {
|
|
470
|
+
clearAll();
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Fetch company addresses from IAM API
|
|
475
|
+
*/
|
|
476
|
+
export async function fetchCompanyAddresses(
|
|
477
|
+
companyId: string,
|
|
478
|
+
accessToken?: string,
|
|
479
|
+
): Promise<CompanyAddress[]> {
|
|
480
|
+
const token = accessToken || getAccessToken();
|
|
481
|
+
if (!token) {
|
|
482
|
+
throw new Error("No access token available");
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
if (!companyId) {
|
|
486
|
+
console.warn("[OAuth] No company ID provided for fetching addresses");
|
|
487
|
+
return [];
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const iamBaseUrl = import.meta.env.VITE_IAM_API_URL;
|
|
491
|
+
|
|
492
|
+
try {
|
|
493
|
+
const response = await fetch(
|
|
494
|
+
`${iamBaseUrl}/api/v1/companies/${companyId}/addresses`,
|
|
495
|
+
{
|
|
496
|
+
method: "GET",
|
|
497
|
+
mode: "cors",
|
|
498
|
+
credentials: "omit",
|
|
499
|
+
headers: {
|
|
500
|
+
Authorization: `Bearer ${token}`,
|
|
501
|
+
Accept: "application/json",
|
|
502
|
+
},
|
|
503
|
+
},
|
|
504
|
+
);
|
|
505
|
+
|
|
506
|
+
if (!response.ok) {
|
|
507
|
+
console.warn(
|
|
508
|
+
"[OAuth] Failed to fetch company addresses:",
|
|
509
|
+
response.status,
|
|
510
|
+
);
|
|
511
|
+
return [];
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
const data = await response.json();
|
|
515
|
+
console.log("[OAuth] Company addresses fetched:", data);
|
|
516
|
+
|
|
517
|
+
return Array.isArray(data) ? data : data.result || data.addresses || [];
|
|
518
|
+
} catch (error) {
|
|
519
|
+
console.error("[OAuth] Error fetching company addresses:", error);
|
|
520
|
+
return [];
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Get primary address country from company addresses
|
|
526
|
+
*/
|
|
527
|
+
export function getPrimaryAddressCountry(
|
|
528
|
+
addresses: CompanyAddress[],
|
|
529
|
+
): string | undefined {
|
|
530
|
+
if (!addresses || addresses.length === 0) {
|
|
531
|
+
return undefined;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
const primaryAddress = addresses.find((addr) => addr.is_primary === true);
|
|
535
|
+
if (primaryAddress && primaryAddress.country) {
|
|
536
|
+
return primaryAddress.country;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// Fallback to first address if no primary found
|
|
540
|
+
const firstAddress = addresses[0];
|
|
541
|
+
return firstAddress?.country;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Fetch company brands from IAM API with pagination
|
|
546
|
+
*/
|
|
547
|
+
export async function fetchCompanyBrands(
|
|
548
|
+
companyId: string,
|
|
549
|
+
options: { page?: number; limit?: number; search?: string } = {},
|
|
550
|
+
accessToken?: string,
|
|
551
|
+
): Promise<CompanyBrandsResponse> {
|
|
552
|
+
const token = accessToken || getAccessToken();
|
|
553
|
+
if (!token) {
|
|
554
|
+
throw new Error("No access token available");
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
if (!companyId) {
|
|
558
|
+
console.warn("[OAuth] No company ID provided for fetching brands");
|
|
559
|
+
return { brands: [], total: 0, page: 1, limit: 20 };
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
const iamBaseUrl = import.meta.env.VITE_IAM_API_URL;
|
|
563
|
+
|
|
564
|
+
if (!iamBaseUrl) {
|
|
565
|
+
console.error("[OAuth] VITE_IAM_API_URL is not configured - cannot fetch brands");
|
|
566
|
+
return { brands: [], total: 0, page: 1, limit: 20 };
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
const params = new URLSearchParams();
|
|
570
|
+
if (options.page) params.append("page", options.page.toString());
|
|
571
|
+
if (options.limit) params.append("limit", options.limit.toString());
|
|
572
|
+
if (options.search) params.append("search", options.search);
|
|
573
|
+
|
|
574
|
+
const queryString = params.toString();
|
|
575
|
+
const url = `${iamBaseUrl}/api/v1/companies/${companyId}/brands${queryString ? `?${queryString}` : ""}`;
|
|
576
|
+
|
|
577
|
+
try {
|
|
578
|
+
const response = await fetch(url, {
|
|
579
|
+
method: "GET",
|
|
580
|
+
mode: "cors",
|
|
581
|
+
credentials: "omit",
|
|
582
|
+
headers: {
|
|
583
|
+
Authorization: `Bearer ${token}`,
|
|
584
|
+
Accept: "application/json",
|
|
585
|
+
},
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
if (!response.ok) {
|
|
589
|
+
console.warn("[OAuth] Failed to fetch company brands:", response.status);
|
|
590
|
+
return { brands: [], total: 0, page: 1, limit: 20 };
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
const data = await response.json();
|
|
594
|
+
console.log("[OAuth] Company brands raw response:", JSON.stringify(data));
|
|
595
|
+
console.log("[OAuth] Response type:", typeof data, "| Keys:", Object.keys(data || {}));
|
|
596
|
+
|
|
597
|
+
// Handle multiple API response structures with detailed logging
|
|
598
|
+
const extractBrandsData = (): { brands: CompanyBrand[]; total: number; page: number; limit: number } | null => {
|
|
599
|
+
// Case 1: { success: true, data: { brands: [...] } }
|
|
600
|
+
if (data?.success === true && data?.data?.brands) {
|
|
601
|
+
console.log("[OAuth] Matched structure: success + data.brands");
|
|
602
|
+
return data.data;
|
|
603
|
+
}
|
|
604
|
+
// Case 2: { success: true, result: { brands: [...] } }
|
|
605
|
+
if (data?.success === true && data?.result?.brands) {
|
|
606
|
+
console.log("[OAuth] Matched structure: success + result.brands");
|
|
607
|
+
return data.result;
|
|
608
|
+
}
|
|
609
|
+
// Case 3: { data: { brands: [...] } } (without success flag)
|
|
610
|
+
if (data?.data?.brands) {
|
|
611
|
+
console.log("[OAuth] Matched structure: data.brands (no success)");
|
|
612
|
+
return data.data;
|
|
613
|
+
}
|
|
614
|
+
// Case 4: { result: { brands: [...] } } (without success flag)
|
|
615
|
+
if (data?.result?.brands) {
|
|
616
|
+
console.log("[OAuth] Matched structure: result.brands (no success)");
|
|
617
|
+
return data.result;
|
|
618
|
+
}
|
|
619
|
+
// Case 5: { brands: [...], total, page, limit } (flat response)
|
|
620
|
+
if (data?.brands && (Array.isArray(data.brands) || typeof data.brands === 'object')) {
|
|
621
|
+
console.log("[OAuth] Matched structure: flat brands array");
|
|
622
|
+
const brands = Array.isArray(data.brands) ? data.brands : [];
|
|
623
|
+
return {
|
|
624
|
+
brands,
|
|
625
|
+
total: data.total ?? brands.length,
|
|
626
|
+
page: data.page ?? 1,
|
|
627
|
+
limit: data.limit ?? 20
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
// Case 6: Direct array response [...]
|
|
631
|
+
if (Array.isArray(data)) {
|
|
632
|
+
console.log("[OAuth] Matched structure: direct array");
|
|
633
|
+
return { brands: data, total: data.length, page: 1, limit: 20 };
|
|
634
|
+
}
|
|
635
|
+
// Case 7: { items: [...] } pattern
|
|
636
|
+
if (data?.items && Array.isArray(data.items)) {
|
|
637
|
+
console.log("[OAuth] Matched structure: items array");
|
|
638
|
+
return {
|
|
639
|
+
brands: data.items,
|
|
640
|
+
total: data.total ?? data.items.length,
|
|
641
|
+
page: data.page ?? 1,
|
|
642
|
+
limit: data.limit ?? 20
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
// Case 8: { rows: [...] } pattern
|
|
646
|
+
if (data?.rows && Array.isArray(data.rows)) {
|
|
647
|
+
console.log("[OAuth] Matched structure: rows array");
|
|
648
|
+
return {
|
|
649
|
+
brands: data.rows,
|
|
650
|
+
total: data.total ?? data.count ?? data.rows.length,
|
|
651
|
+
page: data.page ?? 1,
|
|
652
|
+
limit: data.limit ?? 20
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
return null;
|
|
656
|
+
};
|
|
657
|
+
|
|
658
|
+
const brandsData = extractBrandsData();
|
|
659
|
+
if (brandsData && brandsData.brands.length >= 0) {
|
|
660
|
+
console.log("[OAuth] Extracted brands count:", brandsData.brands.length);
|
|
661
|
+
return {
|
|
662
|
+
brands: brandsData.brands,
|
|
663
|
+
total: brandsData.total,
|
|
664
|
+
page: brandsData.page,
|
|
665
|
+
limit: brandsData.limit,
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
console.warn("[OAuth] No matching brands structure. Raw data:", JSON.stringify(data));
|
|
670
|
+
return { brands: [], total: 0, page: 1, limit: 20 };
|
|
671
|
+
} catch (error) {
|
|
672
|
+
console.warn(
|
|
673
|
+
"[OAuth] Error fetching company brands (likely CORS restriction):",
|
|
674
|
+
error,
|
|
675
|
+
);
|
|
676
|
+
return { brands: [], total: 0, page: 1, limit: 20 };
|
|
677
|
+
}
|
|
678
|
+
}
|