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.
Files changed (525) hide show
  1. package/.ci/staging.yml +191 -0
  2. package/.dockerignore +117 -0
  3. package/.env +40 -0
  4. package/.env.staging +38 -0
  5. package/.gitlab-ci.yml +16 -0
  6. package/DEMO_STATUS.md +579 -0
  7. package/Dockerfile +61 -0
  8. package/Influence-MW-AdServer-12-02-2026/client/index.html +17 -0
  9. package/Influence-MW-AdServer-12-02-2026/client/public/favicon.png +0 -0
  10. package/Influence-MW-AdServer-12-02-2026/client/src/App.tsx +91 -0
  11. package/Influence-MW-AdServer-12-02-2026/client/src/components/advanced-map-drawer.tsx +1131 -0
  12. package/Influence-MW-AdServer-12-02-2026/client/src/components/ai-recommendation-panel.tsx +379 -0
  13. package/Influence-MW-AdServer-12-02-2026/client/src/components/app-sidebar.tsx +183 -0
  14. package/Influence-MW-AdServer-12-02-2026/client/src/components/auto-optimize-button.tsx +184 -0
  15. package/Influence-MW-AdServer-12-02-2026/client/src/components/availability-drawer.tsx +385 -0
  16. package/Influence-MW-AdServer-12-02-2026/client/src/components/brand-insights-panel.tsx +87 -0
  17. package/Influence-MW-AdServer-12-02-2026/client/src/components/create-agency-drawer.tsx +198 -0
  18. package/Influence-MW-AdServer-12-02-2026/client/src/components/create-brand-drawer.tsx +275 -0
  19. package/Influence-MW-AdServer-12-02-2026/client/src/components/creative-assignment.tsx +526 -0
  20. package/Influence-MW-AdServer-12-02-2026/client/src/components/data-table-toolbar.tsx +148 -0
  21. package/Influence-MW-AdServer-12-02-2026/client/src/components/data-table.tsx +158 -0
  22. package/Influence-MW-AdServer-12-02-2026/client/src/components/filter-drawer.tsx +356 -0
  23. package/Influence-MW-AdServer-12-02-2026/client/src/components/form-insights-panel.tsx +82 -0
  24. package/Influence-MW-AdServer-12-02-2026/client/src/components/geography-selector.tsx +699 -0
  25. package/Influence-MW-AdServer-12-02-2026/client/src/components/header-user-menu.tsx +178 -0
  26. package/Influence-MW-AdServer-12-02-2026/client/src/components/history-drawer.tsx +313 -0
  27. package/Influence-MW-AdServer-12-02-2026/client/src/components/inventory-availability-section.tsx +176 -0
  28. package/Influence-MW-AdServer-12-02-2026/client/src/components/inventory-format-drawer.tsx +173 -0
  29. package/Influence-MW-AdServer-12-02-2026/client/src/components/inventory-selector.tsx +401 -0
  30. package/Influence-MW-AdServer-12-02-2026/client/src/components/manual-inventory-drawer.tsx +368 -0
  31. package/Influence-MW-AdServer-12-02-2026/client/src/components/mapbox-map.tsx +368 -0
  32. package/Influence-MW-AdServer-12-02-2026/client/src/components/market-insights-panel.tsx +202 -0
  33. package/Influence-MW-AdServer-12-02-2026/client/src/components/media-owner-drawer.tsx +217 -0
  34. package/Influence-MW-AdServer-12-02-2026/client/src/components/metric-card.tsx +58 -0
  35. package/Influence-MW-AdServer-12-02-2026/client/src/components/page-header.tsx +27 -0
  36. package/Influence-MW-AdServer-12-02-2026/client/src/components/player-status-indicator.tsx +137 -0
  37. package/Influence-MW-AdServer-12-02-2026/client/src/components/poi-targeting-drawer.tsx +298 -0
  38. package/Influence-MW-AdServer-12-02-2026/client/src/components/recommendation-score-badge.tsx +102 -0
  39. package/Influence-MW-AdServer-12-02-2026/client/src/components/recommended-inventories-panel.tsx +248 -0
  40. package/Influence-MW-AdServer-12-02-2026/client/src/components/searchable-combobox.tsx +134 -0
  41. package/Influence-MW-AdServer-12-02-2026/client/src/components/signal-visualizations.tsx +407 -0
  42. package/Influence-MW-AdServer-12-02-2026/client/src/components/status-badge.tsx +35 -0
  43. package/Influence-MW-AdServer-12-02-2026/client/src/components/theme-provider.tsx +73 -0
  44. package/Influence-MW-AdServer-12-02-2026/client/src/components/theme-toggle.tsx +37 -0
  45. package/Influence-MW-AdServer-12-02-2026/client/src/components/traffic-slider.tsx +75 -0
  46. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/accordion.tsx +56 -0
  47. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/alert-dialog.tsx +139 -0
  48. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/alert.tsx +59 -0
  49. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/aspect-ratio.tsx +5 -0
  50. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/avatar.tsx +51 -0
  51. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/badge.tsx +38 -0
  52. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/breadcrumb.tsx +115 -0
  53. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/button.tsx +62 -0
  54. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/calendar.tsx +68 -0
  55. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/card.tsx +85 -0
  56. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/carousel.tsx +260 -0
  57. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/chart.tsx +365 -0
  58. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/checkbox.tsx +28 -0
  59. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/collapsible.tsx +11 -0
  60. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/command.tsx +151 -0
  61. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/context-menu.tsx +198 -0
  62. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/dialog.tsx +122 -0
  63. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/drawer.tsx +118 -0
  64. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/dropdown-menu.tsx +198 -0
  65. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/form.tsx +178 -0
  66. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/hover-card.tsx +29 -0
  67. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/input-otp.tsx +69 -0
  68. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/input.tsx +23 -0
  69. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/label.tsx +24 -0
  70. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/menubar.tsx +256 -0
  71. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/navigation-menu.tsx +128 -0
  72. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/pagination.tsx +117 -0
  73. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/popover.tsx +29 -0
  74. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/progress.tsx +28 -0
  75. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/radio-group.tsx +42 -0
  76. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/resizable.tsx +45 -0
  77. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/scroll-area.tsx +46 -0
  78. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/select.tsx +160 -0
  79. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/separator.tsx +29 -0
  80. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/sheet.tsx +140 -0
  81. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/sidebar.tsx +727 -0
  82. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/skeleton.tsx +15 -0
  83. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/slider.tsx +26 -0
  84. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/switch.tsx +27 -0
  85. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/table.tsx +117 -0
  86. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/tabs.tsx +53 -0
  87. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/textarea.tsx +22 -0
  88. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/toast.tsx +127 -0
  89. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/toaster.tsx +33 -0
  90. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/toggle-group.tsx +61 -0
  91. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/toggle.tsx +43 -0
  92. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/tooltip.tsx +30 -0
  93. package/Influence-MW-AdServer-12-02-2026/client/src/components/vendor-stores-modal.tsx +336 -0
  94. package/Influence-MW-AdServer-12-02-2026/client/src/components/venue-type-drawer.tsx +359 -0
  95. package/Influence-MW-AdServer-12-02-2026/client/src/components/venue-type-selector.tsx +436 -0
  96. package/Influence-MW-AdServer-12-02-2026/client/src/hooks/use-mobile.tsx +19 -0
  97. package/Influence-MW-AdServer-12-02-2026/client/src/hooks/use-toast.ts +191 -0
  98. package/Influence-MW-AdServer-12-02-2026/client/src/index.css +244 -0
  99. package/Influence-MW-AdServer-12-02-2026/client/src/lib/queryClient.ts +57 -0
  100. package/Influence-MW-AdServer-12-02-2026/client/src/lib/utils.ts +39 -0
  101. package/Influence-MW-AdServer-12-02-2026/client/src/lib/venue-taxonomy.ts +532 -0
  102. package/Influence-MW-AdServer-12-02-2026/client/src/main.tsx +5 -0
  103. package/Influence-MW-AdServer-12-02-2026/client/src/pages/assign-creative.tsx +781 -0
  104. package/Influence-MW-AdServer-12-02-2026/client/src/pages/content-hub.tsx +995 -0
  105. package/Influence-MW-AdServer-12-02-2026/client/src/pages/custom-pois.tsx +431 -0
  106. package/Influence-MW-AdServer-12-02-2026/client/src/pages/dashboard.tsx +620 -0
  107. package/Influence-MW-AdServer-12-02-2026/client/src/pages/deal-detail.tsx +1062 -0
  108. package/Influence-MW-AdServer-12-02-2026/client/src/pages/deal-form.tsx +1570 -0
  109. package/Influence-MW-AdServer-12-02-2026/client/src/pages/deals.tsx +716 -0
  110. package/Influence-MW-AdServer-12-02-2026/client/src/pages/edit-creative-assignment.tsx +1051 -0
  111. package/Influence-MW-AdServer-12-02-2026/client/src/pages/geotargeting.tsx +675 -0
  112. package/Influence-MW-AdServer-12-02-2026/client/src/pages/integrations.tsx +425 -0
  113. package/Influence-MW-AdServer-12-02-2026/client/src/pages/line-item-creatives.tsx +622 -0
  114. package/Influence-MW-AdServer-12-02-2026/client/src/pages/line-item-form.tsx +3132 -0
  115. package/Influence-MW-AdServer-12-02-2026/client/src/pages/line-items.tsx +530 -0
  116. package/Influence-MW-AdServer-12-02-2026/client/src/pages/not-found.tsx +21 -0
  117. package/Influence-MW-AdServer-12-02-2026/client/src/pages/proof-of-play-upload.tsx +479 -0
  118. package/Influence-MW-AdServer-12-02-2026/client/src/pages/proof-of-play.tsx +880 -0
  119. package/Influence-MW-AdServer-12-02-2026/client/src/pages/reports.tsx +235 -0
  120. package/Influence-MW-AdServer-12-02-2026/client/src/pages/settings.tsx +652 -0
  121. package/Influence-MW-AdServer-12-02-2026/client/src/pages/signal-form.tsx +1117 -0
  122. package/Influence-MW-AdServer-12-02-2026/client/src/pages/signals.tsx +366 -0
  123. package/Influence-MW-AdServer-12-02-2026/client/src/pages/tags.tsx +332 -0
  124. package/Influence-MW-AdServer-12-02-2026/client/src/pages/venues.tsx +381 -0
  125. package/Influence-MW-AdServer-12-02-2026/client/src/types/mapbox-gl-draw.d.ts +37 -0
  126. package/Influence-MW-AdServer-12-02-2026/client/src/types/react-simple-maps.d.ts +57 -0
  127. package/Influence-MW-AdServer-12-02-2026/components.json +20 -0
  128. package/Influence-MW-AdServer-12-02-2026/docs/PRD.md +3373 -0
  129. package/Influence-MW-AdServer-12-02-2026/docs/influence-feature-mapping.csv +498 -0
  130. package/Influence-MW-AdServer-12-02-2026/drizzle.config.ts +14 -0
  131. package/Influence-MW-AdServer-12-02-2026/package-lock.json +9672 -0
  132. package/Influence-MW-AdServer-12-02-2026/package.json +118 -0
  133. package/Influence-MW-AdServer-12-02-2026/postcss.config.js +6 -0
  134. package/Influence-MW-AdServer-12-02-2026/replit.md +91 -0
  135. package/Influence-MW-AdServer-12-02-2026/script/build.ts +67 -0
  136. package/Influence-MW-AdServer-12-02-2026/scripts/create-miro-diagrams.cjs +318 -0
  137. package/Influence-MW-AdServer-12-02-2026/scripts/create-remaining-diagrams.cjs +270 -0
  138. package/Influence-MW-AdServer-12-02-2026/server/index.ts +103 -0
  139. package/Influence-MW-AdServer-12-02-2026/server/recommendation-service.ts +319 -0
  140. package/Influence-MW-AdServer-12-02-2026/server/routes.ts +1890 -0
  141. package/Influence-MW-AdServer-12-02-2026/server/static.ts +19 -0
  142. package/Influence-MW-AdServer-12-02-2026/server/storage.ts +2058 -0
  143. package/Influence-MW-AdServer-12-02-2026/server/vite.ts +58 -0
  144. package/Influence-MW-AdServer-12-02-2026/shared/schema.ts +1595 -0
  145. package/Influence-MW-AdServer-12-02-2026/tailwind.config.ts +107 -0
  146. package/Influence-MW-AdServer-12-02-2026/tsconfig.json +23 -0
  147. package/Influence-MW-AdServer-12-02-2026/vite.config.ts +40 -0
  148. package/LINE_ITEM_BUDGET_FIELD_MAPPING.md +178 -0
  149. package/PCM/.env.example +92 -0
  150. package/PCM/README.md +558 -0
  151. package/PCM/docs/TEST_CASES.md +422 -0
  152. package/PCM/index.js +106 -0
  153. package/PCM/package-lock.json +3282 -0
  154. package/PCM/package.json +32 -0
  155. package/PCM/replit.md +64 -0
  156. package/PCM/schema.sql +495 -0
  157. package/PCM/scripts/export-schema.js +183 -0
  158. package/PCM/scripts/seed-comprehensive.js +631 -0
  159. package/PCM/scripts/seed-production.js +477 -0
  160. package/PCM/src/config/db.js +56 -0
  161. package/PCM/src/config/swagger.js +5975 -0
  162. package/PCM/src/dto/EmailRequestDTO.js +166 -0
  163. package/PCM/src/middleware/errorHandler.js +52 -0
  164. package/PCM/src/middleware/logger.js +26 -0
  165. package/PCM/src/migrations/001_add_campaign_mode_fields.sql +36 -0
  166. package/PCM/src/migrations/002_create_deal_id_counters.sql +22 -0
  167. package/PCM/src/migrations/003_update_publishers_column.sql +15 -0
  168. package/PCM/src/migrations/004_add_direct_dealtype_and_advertiser.sql +5 -0
  169. package/PCM/src/migrations/005_add_programmatic_fields_and_update_enums.sql +31 -0
  170. package/PCM/src/migrations/006_add_line_item_programmatic_fields.sql +12 -0
  171. package/PCM/src/migrations/007_add_line_item_direct_fields.sql +15 -0
  172. package/PCM/src/migrations/008_add_inventory_fields.sql +45 -0
  173. package/PCM/src/migrations/009_move_inventory_fields_to_metadata.sql +32 -0
  174. package/PCM/src/migrations/010_add_draft_status_and_line_items_count.sql +23 -0
  175. package/PCM/src/migrations/011_add_planning_field.sql +21 -0
  176. package/PCM/src/migrations/012_fix_inventory_composite_pk.sql +17 -0
  177. package/PCM/src/migrations/013_make_external_id_optional.sql +3 -0
  178. package/PCM/src/migrations/014_create_change_history.sql +38 -0
  179. package/PCM/src/migrations/016_create_publisher_insertion_orders.sql +33 -0
  180. package/PCM/src/migrations/017_fix_line_item_id_fk_reference.sql +86 -0
  181. package/PCM/src/migrations/018_create_approval_tables.sql +44 -0
  182. package/PCM/src/migrations/019_add_encrypted_token_column.sql +2 -0
  183. package/PCM/src/migrations/020_add_rejection_reason_to_deals.sql +10 -0
  184. package/PCM/src/migrations/021_add_publisher_external_id_to_inventories.sql +12 -0
  185. package/PCM/src/migrations/022_add_line_item_extended_fields.sql +24 -0
  186. package/PCM/src/migrations/023_add_base_price_fields.sql +8 -0
  187. package/PCM/src/migrations/run-migrations.js +46 -0
  188. package/PCM/src/models/ApprovalOTP.js +51 -0
  189. package/PCM/src/models/ApprovalToken.js +79 -0
  190. package/PCM/src/models/ChangeHistory.js +107 -0
  191. package/PCM/src/models/Deal.js +186 -0
  192. package/PCM/src/models/DealIdCounter.js +28 -0
  193. package/PCM/src/models/LineItem.js +227 -0
  194. package/PCM/src/models/LineItemCreative.js +89 -0
  195. package/PCM/src/models/LineItemInventory.js +115 -0
  196. package/PCM/src/models/PublisherInsertionOrder.js +93 -0
  197. package/PCM/src/models/TransactionHistory.js +34 -0
  198. package/PCM/src/models/associations.js +81 -0
  199. package/PCM/src/routes/approval.js +321 -0
  200. package/PCM/src/routes/creatives.js +437 -0
  201. package/PCM/src/routes/deals.js +1638 -0
  202. package/PCM/src/routes/digitalSignage.js +242 -0
  203. package/PCM/src/routes/insertionOrders.js +380 -0
  204. package/PCM/src/routes/lineItems.js +926 -0
  205. package/PCM/src/routes/system.js +384 -0
  206. package/PCM/src/services/ApprovalService.js +885 -0
  207. package/PCM/src/services/CampaignImportConverter.js +631 -0
  208. package/PCM/src/services/CampaignModeService.js +273 -0
  209. package/PCM/src/services/CampaignStatusService.js +395 -0
  210. package/PCM/src/services/ChangeHistoryService.js +316 -0
  211. package/PCM/src/services/DealIdService.js +94 -0
  212. package/PCM/src/services/DealResponseFormatter.js +90 -0
  213. package/PCM/src/services/EmailNotificationService.js +315 -0
  214. package/PCM/src/services/LineItemResponseFormatter.js +122 -0
  215. package/PCM/src/services/LineItemStatusService.js +380 -0
  216. package/PCM/src/tests/comprehensiveTestRunner.js +360 -0
  217. package/PCM/src/tests/comprehensiveTests.js +1277 -0
  218. package/PCM/src/tests/dealTypeUnitTests.js +1058 -0
  219. package/PCM/src/tests/testRunner.js +248 -0
  220. package/PCM/src/utils/caseConverter.js +92 -0
  221. package/PCM/src/utils/dealCalculations.js +206 -0
  222. package/PCM/src/utils/lineItemPayloadNormalizer.js +41 -0
  223. package/PCM/src/utils/payloadNormalizer.js +34 -0
  224. package/PCM/src/utils/sourceNormalizer.js +56 -0
  225. package/PCM/src/validators/creativeValidator.js +27 -0
  226. package/PCM/src/validators/dealValidator.js +203 -0
  227. package/PCM/src/validators/lineItemValidator.js +489 -0
  228. package/PCM/tests/approval-flows.test.js +238 -0
  229. package/PCM/tests/approval-workflow.test.js +291 -0
  230. package/PCM/tests/campaign-import-converter.test.js +543 -0
  231. package/PCM/tests/campaign-import-e2e.test.js +520 -0
  232. package/PCM/tests/campaign-status.test.js +539 -0
  233. package/PCM/tests/direct-publisher-split-reimport.test.js +460 -0
  234. package/PCM/tests/e2e/digital-signage.test.js +145 -0
  235. package/PCM/tests/e2e/search-filter-pagination.test.js +399 -0
  236. package/PCM/tests/e2e-comprehensive.test.js +3446 -0
  237. package/PCM/tests/edge-cases.test.js +340 -0
  238. package/PCM/tests/line-item-status.test.js +340 -0
  239. package/PCM/tests/seller-account-external-ids.test.js +877 -0
  240. package/PCM/tests/source-validation.test.js +324 -0
  241. package/PRD.md +3373 -0
  242. package/README.md +186 -0
  243. package/client/index.html +35 -0
  244. package/client/public/DEMO_STATUS.md +579 -0
  245. package/client/public/img/MW-logo-trans_1754045676555.png +0 -0
  246. package/client/public/locales/ar/approval.json +144 -0
  247. package/client/public/locales/ar/buyer.json +61 -0
  248. package/client/public/locales/ar/campaigns.json +1 -0
  249. package/client/public/locales/ar/common.json +218 -0
  250. package/client/public/locales/ar/contentHub.json +266 -0
  251. package/client/public/locales/ar/creatives.json +79 -0
  252. package/client/public/locales/ar/dashboard.json +57 -0
  253. package/client/public/locales/ar/deals.json +886 -0
  254. package/client/public/locales/ar/dsp.json +131 -0
  255. package/client/public/locales/ar/inventory.json +201 -0
  256. package/client/public/locales/ar/lineItems.json +553 -0
  257. package/client/public/locales/ar/navigation.json +48 -0
  258. package/client/public/locales/ar/wizard.json +1 -0
  259. package/client/public/locales/en/approval.json +144 -0
  260. package/client/public/locales/en/buyer.json +65 -0
  261. package/client/public/locales/en/campaigns.json +1 -0
  262. package/client/public/locales/en/common.json +218 -0
  263. package/client/public/locales/en/contentHub.json +266 -0
  264. package/client/public/locales/en/creatives.json +79 -0
  265. package/client/public/locales/en/dashboard.json +57 -0
  266. package/client/public/locales/en/deals.json +886 -0
  267. package/client/public/locales/en/dsp.json +131 -0
  268. package/client/public/locales/en/inventory.json +201 -0
  269. package/client/public/locales/en/lineItems.json +659 -0
  270. package/client/public/locales/en/navigation.json +48 -0
  271. package/client/public/locales/en/wizard.json +1 -0
  272. package/client/public/locales/ja/approval.json +144 -0
  273. package/client/public/locales/ja/buyer.json +61 -0
  274. package/client/public/locales/ja/campaigns.json +1 -0
  275. package/client/public/locales/ja/common.json +218 -0
  276. package/client/public/locales/ja/contentHub.json +266 -0
  277. package/client/public/locales/ja/creatives.json +79 -0
  278. package/client/public/locales/ja/dashboard.json +57 -0
  279. package/client/public/locales/ja/deals.json +886 -0
  280. package/client/public/locales/ja/dsp.json +131 -0
  281. package/client/public/locales/ja/inventory.json +201 -0
  282. package/client/public/locales/ja/lineItems.json +553 -0
  283. package/client/public/locales/ja/navigation.json +48 -0
  284. package/client/public/locales/ja/wizard.json +1 -0
  285. package/client/public/locales/zh/approval.json +144 -0
  286. package/client/public/locales/zh/buyer.json +61 -0
  287. package/client/public/locales/zh/campaigns.json +1 -0
  288. package/client/public/locales/zh/common.json +218 -0
  289. package/client/public/locales/zh/contentHub.json +266 -0
  290. package/client/public/locales/zh/creatives.json +79 -0
  291. package/client/public/locales/zh/dashboard.json +57 -0
  292. package/client/public/locales/zh/deals.json +886 -0
  293. package/client/public/locales/zh/dsp.json +131 -0
  294. package/client/public/locales/zh/inventory.json +201 -0
  295. package/client/public/locales/zh/lineItems.json +553 -0
  296. package/client/public/locales/zh/navigation.json +48 -0
  297. package/client/public/locales/zh/wizard.json +1 -0
  298. package/client/public/manifest.json +36 -0
  299. package/client/src/App.tsx +464 -0
  300. package/client/src/components/app-sidebar.tsx +312 -0
  301. package/client/src/components/approval/approval-decision-form.test.tsx +294 -0
  302. package/client/src/components/approval/approval-decision-form.tsx +326 -0
  303. package/client/src/components/approval/approval-sheet.tsx +631 -0
  304. package/client/src/components/approval/line-item-details-sheet.tsx +371 -0
  305. package/client/src/components/approval/otp-verification.test.tsx +337 -0
  306. package/client/src/components/approval/otp-verification.tsx +180 -0
  307. package/client/src/components/content-hub/bulk-transcode-dialog.tsx +379 -0
  308. package/client/src/components/content-hub/content-hub-manager-v2.tsx +574 -0
  309. package/client/src/components/content-hub/content-hub-manager.tsx +330 -0
  310. package/client/src/components/content-hub/creative-card.tsx +456 -0
  311. package/client/src/components/content-hub/creative-detail-sheet.tsx +685 -0
  312. package/client/src/components/content-hub/creative-filters.tsx +457 -0
  313. package/client/src/components/content-hub/creative-grid.tsx +329 -0
  314. package/client/src/components/content-hub/creative-selector.tsx +415 -0
  315. package/client/src/components/content-hub/creative-upload.tsx +547 -0
  316. package/client/src/components/content-hub/folder-dialogs.tsx +445 -0
  317. package/client/src/components/content-hub/folder-list.tsx +280 -0
  318. package/client/src/components/content-hub/review-dialogs.tsx +268 -0
  319. package/client/src/components/content-hub/transcode-dialog.tsx +226 -0
  320. package/client/src/components/creative-library/creative-details-view.tsx +446 -0
  321. package/client/src/components/creative-library/creative-filters-panel.tsx +203 -0
  322. package/client/src/components/creative-library/creative-list.tsx +360 -0
  323. package/client/src/components/creative-library/creative-status-badge.tsx +71 -0
  324. package/client/src/components/creative-library/folder-card.tsx +78 -0
  325. package/client/src/components/creative-library/index.ts +27 -0
  326. package/client/src/components/creative-library/new-creative-card.tsx +211 -0
  327. package/client/src/components/creative-library/upload-creative-dialog.tsx +261 -0
  328. package/client/src/components/dashboard-overview.tsx +109 -0
  329. package/client/src/components/deals/approval-history-panel.test.tsx +240 -0
  330. package/client/src/components/deals/approval-history-panel.tsx +156 -0
  331. package/client/src/components/deals/deal-status-badge.tsx +92 -0
  332. package/client/src/components/deals/import-from-planner-dialog.tsx +399 -0
  333. package/client/src/components/deals/market-insights-panel.tsx +237 -0
  334. package/client/src/components/deals/reopen-deal-sheet.tsx +191 -0
  335. package/client/src/components/deals/request-approval-sheet.test.tsx +323 -0
  336. package/client/src/components/deals/request-approval-sheet.tsx +136 -0
  337. package/client/src/components/deals/resend-approval-sheet.tsx +201 -0
  338. package/client/src/components/direct-campaigns/campaign-card.tsx +283 -0
  339. package/client/src/components/direct-campaigns/deal-filter-panel.tsx +325 -0
  340. package/client/src/components/inventory/advanced-filters-panel.tsx +273 -0
  341. package/client/src/components/inventory/csv-upload-modal.tsx +639 -0
  342. package/client/src/components/inventory/inventory-availability-view.tsx +486 -0
  343. package/client/src/components/inventory/inventory-details-sheet.tsx +376 -0
  344. package/client/src/components/inventory/inventory-map-view.tsx +596 -0
  345. package/client/src/components/inventory/inventory-settings-menu.tsx +52 -0
  346. package/client/src/components/language-switcher.tsx +53 -0
  347. package/client/src/components/line-items/campaign-forecast-panel.tsx +138 -0
  348. package/client/src/components/line-items/form-insights.tsx +89 -0
  349. package/client/src/components/line-items/geofencing/LocationCsvUploadDrawer.tsx +100 -0
  350. package/client/src/components/line-items/geofencing/POIDropdown.tsx +379 -0
  351. package/client/src/components/line-items/geofencing/SelectedLocationsSidebar.tsx +436 -0
  352. package/client/src/components/line-items/geofencing/ViewFileLocationDrawer.tsx +199 -0
  353. package/client/src/components/line-items/geofencing/components/ExistingFilesTab.tsx +268 -0
  354. package/client/src/components/line-items/geofencing/components/TemplateDownloadSection.tsx +59 -0
  355. package/client/src/components/line-items/geofencing/components/UploadTab.tsx +215 -0
  356. package/client/src/components/line-items/geofencing-map.tsx +1270 -0
  357. package/client/src/components/line-items/inventory-availability-section.tsx +178 -0
  358. package/client/src/components/line-items/line-item-schedule-manager.tsx +313 -0
  359. package/client/src/components/line-items/manual-inventory-drawer.tsx +346 -0
  360. package/client/src/components/line-items/planner-inventory-card.tsx +495 -0
  361. package/client/src/components/line-items/planner-schedule-grid.tsx +495 -0
  362. package/client/src/components/line-items/schedule-rule-editor.tsx +649 -0
  363. package/client/src/components/line-items/schedule-rule-types.ts +122 -0
  364. package/client/src/components/line-items/steps/creatives-step.tsx +681 -0
  365. package/client/src/components/line-items/steps/inventory-schedule-step.tsx +1596 -0
  366. package/client/src/components/line-items/steps/inventory-step.tsx +1533 -0
  367. package/client/src/components/line-items/steps/line-item-details-step.tsx +916 -0
  368. package/client/src/components/line-items/steps/schedule-step.tsx +273 -0
  369. package/client/src/components/line-items/steps/summary-step.tsx +680 -0
  370. package/client/src/components/line-items/steps/targeting-step.tsx +1708 -0
  371. package/client/src/components/product-switcher.tsx +105 -0
  372. package/client/src/components/protected-route.tsx +49 -0
  373. package/client/src/components/skip-link.tsx +22 -0
  374. package/client/src/components/stat-card.tsx +53 -0
  375. package/client/src/components/status-badge.tsx +96 -0
  376. package/client/src/components/ui/hierarchical-venue-selector.tsx +389 -0
  377. package/client/src/components/ui/toaster.tsx +111 -0
  378. package/client/src/contexts/auth-context.tsx +181 -0
  379. package/client/src/contexts/sidebar-state.tsx +50 -0
  380. package/client/src/contexts/theme-context.tsx +66 -0
  381. package/client/src/data/campaign-data.json +107 -0
  382. package/client/src/data/countries.json +22 -0
  383. package/client/src/hooks/use-approval.ts +366 -0
  384. package/client/src/hooks/use-keyboard-shortcuts.ts +74 -0
  385. package/client/src/hooks/use-media-query.ts +46 -0
  386. package/client/src/hooks/use-mobile.tsx +19 -0
  387. package/client/src/hooks/use-page-title.ts +21 -0
  388. package/client/src/hooks/use-toast.ts +195 -0
  389. package/client/src/index.css +694 -0
  390. package/client/src/lib/__tests__/accessibility.test.ts +104 -0
  391. package/client/src/lib/__tests__/date-utils.test.ts +199 -0
  392. package/client/src/lib/__tests__/dsp-buyer-api.test.ts +127 -0
  393. package/client/src/lib/__tests__/dsp-buyer-integration.test.ts +247 -0
  394. package/client/src/lib/__tests__/storage-utils.test.ts +167 -0
  395. package/client/src/lib/__tests__/utils.test.ts +57 -0
  396. package/client/src/lib/accessibility.ts +141 -0
  397. package/client/src/lib/api-config.ts +9 -0
  398. package/client/src/lib/auth-service.ts +209 -0
  399. package/client/src/lib/campaign-creative-api.ts +82 -0
  400. package/client/src/lib/company-api.ts +61 -0
  401. package/client/src/lib/content-hub-api.ts +407 -0
  402. package/client/src/lib/creative-mapper.ts +61 -0
  403. package/client/src/lib/date-utils.ts +119 -0
  404. package/client/src/lib/deal-helpers.ts +220 -0
  405. package/client/src/lib/dsp-buyer-api.ts +196 -0
  406. package/client/src/lib/geo-import-api.ts +151 -0
  407. package/client/src/lib/google-poi-api.ts +305 -0
  408. package/client/src/lib/i18n/__tests__/formatting.test.ts +202 -0
  409. package/client/src/lib/i18n/formatting.ts +130 -0
  410. package/client/src/lib/i18n/index.ts +8 -0
  411. package/client/src/lib/i18n-compat.ts +76 -0
  412. package/client/src/lib/influence-deals-api.ts +896 -0
  413. package/client/src/lib/inventory-api.ts +399 -0
  414. package/client/src/lib/oauth-service.ts +678 -0
  415. package/client/src/lib/poi-types.ts +75 -0
  416. package/client/src/lib/queryClient.ts +144 -0
  417. package/client/src/lib/recommendation-api.ts +380 -0
  418. package/client/src/lib/storage-utils.ts +104 -0
  419. package/client/src/lib/tolgee.ts +85 -0
  420. package/client/src/lib/utils.ts +0 -0
  421. package/client/src/main.tsx +67 -0
  422. package/client/src/mapbox-draw-modes.d.ts +32 -0
  423. package/client/src/pages/all-folders.tsx +203 -0
  424. package/client/src/pages/auth-callback.tsx +115 -0
  425. package/client/src/pages/buyer-form.tsx +339 -0
  426. package/client/src/pages/buyer-list.tsx +622 -0
  427. package/client/src/pages/content-hub.tsx +1358 -0
  428. package/client/src/pages/create-deal.tsx +2093 -0
  429. package/client/src/pages/creative-assignment-page.tsx +548 -0
  430. package/client/src/pages/creatives.tsx +5 -0
  431. package/client/src/pages/custom-pois.tsx +425 -0
  432. package/client/src/pages/dashboard.tsx +615 -0
  433. package/client/src/pages/deal-history.tsx +434 -0
  434. package/client/src/pages/deal-line-items.tsx +1703 -0
  435. package/client/src/pages/demo-status.tsx +113 -0
  436. package/client/src/pages/direct-campaign-details.tsx +361 -0
  437. package/client/src/pages/direct-campaigns-new.tsx +824 -0
  438. package/client/src/pages/dsp-form.tsx +803 -0
  439. package/client/src/pages/dsp-list.tsx +239 -0
  440. package/client/src/pages/folder-content.tsx +336 -0
  441. package/client/src/pages/integrations.tsx +429 -0
  442. package/client/src/pages/line-item-creatives.tsx +789 -0
  443. package/client/src/pages/line-item-detail-page.tsx +684 -0
  444. package/client/src/pages/line-item-form-page.tsx +3261 -0
  445. package/client/src/pages/line-item-wizard.tsx +1207 -0
  446. package/client/src/pages/login.tsx +154 -0
  447. package/client/src/pages/not-found.tsx +23 -0
  448. package/client/src/pages/proof-of-play.tsx +397 -0
  449. package/client/src/pages/public-approval.tsx +551 -0
  450. package/client/src/pages/reports.tsx +231 -0
  451. package/client/src/pages/settings.tsx +760 -0
  452. package/client/src/pages/signals.tsx +389 -0
  453. package/client/src/pages/tags.tsx +318 -0
  454. package/client/src/pages/test-results.tsx +328 -0
  455. package/client/src/store/hooks.ts +5 -0
  456. package/client/src/store/index.ts +15 -0
  457. package/client/src/store/mapMarkerLocationsSlice.ts +241 -0
  458. package/client/src/styles/design-tokens.css +324 -0
  459. package/client/src/test/setup.ts +261 -0
  460. package/client/src/test/test-utils.tsx +40 -0
  461. package/client/src/types/approval.ts +221 -0
  462. package/client/src/types/content-hub.ts +209 -0
  463. package/client/src/types/geofencing.ts +67 -0
  464. package/client/src/types/transcoding.ts +140 -0
  465. package/client/src/vite-env.d.ts +18 -0
  466. package/components.json +20 -0
  467. package/creative-api.json +1 -0
  468. package/docs/AI_REFERENCE.md +459 -0
  469. package/docs/MWDesign-Prompt.md +132 -0
  470. package/docs/MWDesign-System.md +344 -0
  471. package/docs/test-plan.md +277 -0
  472. package/e2e/AUTONOMOUS-TESTING.md +406 -0
  473. package/e2e/README.md +219 -0
  474. package/e2e/autonomous-flow.spec.ts +308 -0
  475. package/e2e/debug-sso.spec.ts +163 -0
  476. package/e2e/direct-campaigns.spec.ts +219 -0
  477. package/e2e/explore-sso.spec.ts +149 -0
  478. package/e2e/fixtures/auth.ts +26 -0
  479. package/e2e/fixtures/enhanced-test.ts +331 -0
  480. package/e2e/pagination.spec.ts +280 -0
  481. package/e2e/view-toggle.spec.ts +312 -0
  482. package/generated-icon.png +0 -0
  483. package/i18next-scanner.config.cjs +46 -0
  484. package/package.json +141 -0
  485. package/playwright.config.ts +93 -0
  486. package/postcss.config.js +6 -0
  487. package/replit.md +196 -0
  488. package/screenshot-after-login.png +0 -0
  489. package/screenshot-contenthub-grid.png +0 -0
  490. package/screenshot-contenthub-list-fixed.png +0 -0
  491. package/screenshot-contenthub-list.png +0 -0
  492. package/screenshot-create-deal.png +0 -0
  493. package/screenshot-dashboard.png +0 -0
  494. package/screenshot-deals.png +0 -0
  495. package/screenshot-login-filled.png +0 -0
  496. package/screenshot-login.png +0 -0
  497. package/screenshot.mjs +24 -0
  498. package/scripts/deploy-stg.sh +185 -0
  499. package/shared/direct-io-schema.ts +383 -0
  500. package/shared/schema.ts +439 -0
  501. package/shared/screen-types.ts +149 -0
  502. package/springdocDefault.json +1 -0
  503. package/swagger-ui-bundle.js +2 -0
  504. package/swagger-ui-init.js +10316 -0
  505. package/tailwind.config.ts +282 -0
  506. package/terraform/README.md +306 -0
  507. package/terraform/cloudfront.tf +289 -0
  508. package/terraform/ecs.tf +727 -0
  509. package/terraform/environments/dev.tfvars +59 -0
  510. package/terraform/environments/production.tfvars +60 -0
  511. package/terraform/main.tf +47 -0
  512. package/terraform/outputs.tf +145 -0
  513. package/terraform/s3.tf +192 -0
  514. package/terraform/variables.tf +226 -0
  515. package/terraform/waf.tf +165 -0
  516. package/terraform-frontend/.terraform.lock.hcl +25 -0
  517. package/terraform-frontend/README.md +85 -0
  518. package/terraform-frontend/cloudfront.tf +125 -0
  519. package/terraform-frontend/main.tf +31 -0
  520. package/terraform-frontend/outputs.tf +24 -0
  521. package/terraform-frontend/terraform.tfvars +12 -0
  522. package/terraform-frontend/variables.tf +53 -0
  523. package/tsconfig.json +23 -0
  524. package/vite.config.ts +226 -0
  525. 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
+ }