@open-mercato/core 0.6.8-develop.6983.1.e4f794099e → 0.6.8-develop.6985.1.fb93574faa

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 (597) hide show
  1. package/.turbo/turbo-build.log +2 -2
  2. package/dist/generated/entities/notification/index.js +6 -0
  3. package/dist/generated/entities/notification/index.js.map +2 -2
  4. package/dist/generated/entities/notification_preference/index.js +19 -0
  5. package/dist/generated/entities/notification_preference/index.js.map +7 -0
  6. package/dist/generated/entities/notification_type/index.js +21 -0
  7. package/dist/generated/entities/notification_type/index.js.map +7 -0
  8. package/dist/generated/entities/notification_type_override/index.js +17 -0
  9. package/dist/generated/entities/notification_type_override/index.js.map +7 -0
  10. package/dist/generated/entities/push_notification_delivery/index.js +41 -0
  11. package/dist/generated/entities/push_notification_delivery/index.js.map +7 -0
  12. package/dist/generated/entities/user_device/index.js +35 -0
  13. package/dist/generated/entities/user_device/index.js.map +7 -0
  14. package/dist/generated/entities.ids.generated.js +12 -1
  15. package/dist/generated/entities.ids.generated.js.map +2 -2
  16. package/dist/generated/entity-fields-registry.js +72 -0
  17. package/dist/generated/entity-fields-registry.js.map +2 -2
  18. package/dist/helpers/integration/api.js +2 -1
  19. package/dist/helpers/integration/api.js.map +2 -2
  20. package/dist/helpers/integration/appRoot.js +9 -0
  21. package/dist/helpers/integration/appRoot.js.map +7 -0
  22. package/dist/helpers/integration/dbFixtures.js +1 -4
  23. package/dist/helpers/integration/dbFixtures.js.map +2 -2
  24. package/dist/helpers/integration/pushFake.js +126 -0
  25. package/dist/helpers/integration/pushFake.js.map +7 -0
  26. package/dist/modules/auth/notifications.js +16 -0
  27. package/dist/modules/auth/notifications.js.map +2 -2
  28. package/dist/modules/business_rules/notifications.js +2 -0
  29. package/dist/modules/business_rules/notifications.js.map +2 -2
  30. package/dist/modules/catalog/notifications.js +2 -0
  31. package/dist/modules/catalog/notifications.js.map +2 -2
  32. package/dist/modules/communication_channels/acl.js +9 -0
  33. package/dist/modules/communication_channels/acl.js.map +2 -2
  34. package/dist/modules/communication_channels/api/delete/admin/channels/[id]/route.js +133 -0
  35. package/dist/modules/communication_channels/api/delete/admin/channels/[id]/route.js.map +7 -0
  36. package/dist/modules/communication_channels/api/get/channels/[id]/health/route.js +6 -2
  37. package/dist/modules/communication_channels/api/get/channels/[id]/health/route.js.map +2 -2
  38. package/dist/modules/communication_channels/api/get/channels/[id]/route.js +6 -2
  39. package/dist/modules/communication_channels/api/get/channels/[id]/route.js.map +2 -2
  40. package/dist/modules/communication_channels/api/get/channels/route.js +2 -1
  41. package/dist/modules/communication_channels/api/get/channels/route.js.map +2 -2
  42. package/dist/modules/communication_channels/api/post/channels/[id]/test-send/route.js +10 -5
  43. package/dist/modules/communication_channels/api/post/channels/[id]/test-send/route.js.map +2 -2
  44. package/dist/modules/communication_channels/api/post/channels/connect/credentials/route.js +29 -1
  45. package/dist/modules/communication_channels/api/post/channels/connect/credentials/route.js.map +2 -2
  46. package/dist/modules/communication_channels/api/post/channels/connect/tenant-credentials/route.js +157 -0
  47. package/dist/modules/communication_channels/api/post/channels/connect/tenant-credentials/route.js.map +7 -0
  48. package/dist/modules/communication_channels/backend/communication_channels/channels/page.js +8 -1
  49. package/dist/modules/communication_channels/backend/communication_channels/channels/page.js.map +2 -2
  50. package/dist/modules/communication_channels/commands/admin-delete-channel.js +141 -0
  51. package/dist/modules/communication_channels/commands/admin-delete-channel.js.map +7 -0
  52. package/dist/modules/communication_channels/commands/connect-credential-channel.js +25 -8
  53. package/dist/modules/communication_channels/commands/connect-credential-channel.js.map +2 -2
  54. package/dist/modules/communication_channels/commands/deliver-outbound-message.js +2 -2
  55. package/dist/modules/communication_channels/commands/deliver-outbound-message.js.map +2 -2
  56. package/dist/modules/communication_channels/commands/push-register.js +1 -1
  57. package/dist/modules/communication_channels/commands/push-register.js.map +2 -2
  58. package/dist/modules/communication_channels/commands/push-unregister.js +4 -3
  59. package/dist/modules/communication_channels/commands/push-unregister.js.map +2 -2
  60. package/dist/modules/communication_channels/data/entities.js +4 -0
  61. package/dist/modules/communication_channels/data/entities.js.map +2 -2
  62. package/dist/modules/communication_channels/data/validators.js +2 -1
  63. package/dist/modules/communication_channels/data/validators.js.map +2 -2
  64. package/dist/modules/communication_channels/lib/access-control.js +11 -1
  65. package/dist/modules/communication_channels/lib/access-control.js.map +2 -2
  66. package/dist/modules/communication_channels/lib/connect-channel.js +20 -12
  67. package/dist/modules/communication_channels/lib/connect-channel.js.map +2 -2
  68. package/dist/modules/communication_channels/lib/push-adapter.js +68 -0
  69. package/dist/modules/communication_channels/lib/push-adapter.js.map +7 -0
  70. package/dist/modules/communication_channels/lib/push-capabilities.js +31 -0
  71. package/dist/modules/communication_channels/lib/push-capabilities.js.map +7 -0
  72. package/dist/modules/communication_channels/lib/push-connect-error.js +20 -0
  73. package/dist/modules/communication_channels/lib/push-connect-error.js.map +7 -0
  74. package/dist/modules/communication_channels/lib/push-credential-errors.js +41 -0
  75. package/dist/modules/communication_channels/lib/push-credential-errors.js.map +7 -0
  76. package/dist/modules/communication_channels/lib/push-envelope.js +46 -0
  77. package/dist/modules/communication_channels/lib/push-envelope.js.map +7 -0
  78. package/dist/modules/communication_channels/lib/refcounted-client-cache.js +64 -0
  79. package/dist/modules/communication_channels/lib/refcounted-client-cache.js.map +7 -0
  80. package/dist/modules/communication_channels/migrations/Migration20260703123630_communication_channels.js +13 -0
  81. package/dist/modules/communication_channels/migrations/Migration20260703123630_communication_channels.js.map +7 -0
  82. package/dist/modules/communication_channels/notifications.js +4 -0
  83. package/dist/modules/communication_channels/notifications.js.map +2 -2
  84. package/dist/modules/communication_channels/setup.js +2 -0
  85. package/dist/modules/communication_channels/setup.js.map +2 -2
  86. package/dist/modules/configs/lib/upgrade-actions.js +21 -0
  87. package/dist/modules/configs/lib/upgrade-actions.js.map +2 -2
  88. package/dist/modules/customer_accounts/notifications.js +12 -0
  89. package/dist/modules/customer_accounts/notifications.js.map +2 -2
  90. package/dist/modules/customers/notifications.js +4 -0
  91. package/dist/modules/customers/notifications.js.map +2 -2
  92. package/dist/modules/devices/acl.js +11 -0
  93. package/dist/modules/devices/acl.js.map +7 -0
  94. package/dist/modules/devices/api/[id]/route.js +167 -0
  95. package/dist/modules/devices/api/[id]/route.js.map +7 -0
  96. package/dist/modules/devices/api/admin/devices/[id]/route.js +219 -0
  97. package/dist/modules/devices/api/admin/devices/[id]/route.js.map +7 -0
  98. package/dist/modules/devices/api/admin/devices/route.js +140 -0
  99. package/dist/modules/devices/api/admin/devices/route.js.map +7 -0
  100. package/dist/modules/devices/api/auth.js +9 -0
  101. package/dist/modules/devices/api/auth.js.map +7 -0
  102. package/dist/modules/devices/api/deviceList.js +53 -0
  103. package/dist/modules/devices/api/deviceList.js.map +7 -0
  104. package/dist/modules/devices/api/deviceOps.js +118 -0
  105. package/dist/modules/devices/api/deviceOps.js.map +7 -0
  106. package/dist/modules/devices/api/openapi.js +19 -0
  107. package/dist/modules/devices/api/openapi.js.map +7 -0
  108. package/dist/modules/devices/api/route.js +143 -0
  109. package/dist/modules/devices/api/route.js.map +7 -0
  110. package/dist/modules/devices/backend/devices/[id]/page.js +135 -0
  111. package/dist/modules/devices/backend/devices/[id]/page.js.map +7 -0
  112. package/dist/modules/devices/backend/devices/[id]/page.meta.js +16 -0
  113. package/dist/modules/devices/backend/devices/[id]/page.meta.js.map +7 -0
  114. package/dist/modules/devices/backend/devices/create/page.js +68 -0
  115. package/dist/modules/devices/backend/devices/create/page.js.map +7 -0
  116. package/dist/modules/devices/backend/devices/create/page.meta.js +16 -0
  117. package/dist/modules/devices/backend/devices/create/page.meta.js.map +7 -0
  118. package/dist/modules/devices/backend/devices/page.js +235 -0
  119. package/dist/modules/devices/backend/devices/page.js.map +7 -0
  120. package/dist/modules/devices/backend/devices/page.meta.js +23 -0
  121. package/dist/modules/devices/backend/devices/page.meta.js.map +7 -0
  122. package/dist/modules/devices/commands/deactivate.js +107 -0
  123. package/dist/modules/devices/commands/deactivate.js.map +7 -0
  124. package/dist/modules/devices/commands/index.js +4 -0
  125. package/dist/modules/devices/commands/index.js.map +7 -0
  126. package/dist/modules/devices/commands/register.js +183 -0
  127. package/dist/modules/devices/commands/register.js.map +7 -0
  128. package/dist/modules/devices/commands/shared.js +74 -0
  129. package/dist/modules/devices/commands/shared.js.map +7 -0
  130. package/dist/modules/devices/commands/update.js +126 -0
  131. package/dist/modules/devices/commands/update.js.map +7 -0
  132. package/dist/modules/devices/data/entities.js +83 -0
  133. package/dist/modules/devices/data/entities.js.map +7 -0
  134. package/dist/modules/devices/data/validators.js +49 -0
  135. package/dist/modules/devices/data/validators.js.map +7 -0
  136. package/dist/modules/devices/di.js +11 -0
  137. package/dist/modules/devices/di.js.map +7 -0
  138. package/dist/modules/devices/encryption.js +12 -0
  139. package/dist/modules/devices/encryption.js.map +7 -0
  140. package/dist/modules/devices/events.js +19 -0
  141. package/dist/modules/devices/events.js.map +7 -0
  142. package/dist/modules/devices/index.js +17 -0
  143. package/dist/modules/devices/index.js.map +7 -0
  144. package/dist/modules/devices/lib/operationMetadata.js +20 -0
  145. package/dist/modules/devices/lib/operationMetadata.js.map +7 -0
  146. package/dist/modules/devices/migrations/Migration20260602120000.js +15 -0
  147. package/dist/modules/devices/migrations/Migration20260602120000.js.map +7 -0
  148. package/dist/modules/devices/migrations/Migration20260630120000.js +15 -0
  149. package/dist/modules/devices/migrations/Migration20260630120000.js.map +7 -0
  150. package/dist/modules/devices/migrations/Migration20260701120000.js +13 -0
  151. package/dist/modules/devices/migrations/Migration20260701120000.js.map +7 -0
  152. package/dist/modules/devices/migrations/Migration20260721120000.js +15 -0
  153. package/dist/modules/devices/migrations/Migration20260721120000.js.map +7 -0
  154. package/dist/modules/devices/setup.js +13 -0
  155. package/dist/modules/devices/setup.js.map +7 -0
  156. package/dist/modules/entities/cli.js +8 -3
  157. package/dist/modules/entities/cli.js.map +2 -2
  158. package/dist/modules/entities/migrations/Migration20260722120000.js +28 -0
  159. package/dist/modules/entities/migrations/Migration20260722120000.js.map +7 -0
  160. package/dist/modules/inbox_ops/notifications.js +2 -0
  161. package/dist/modules/inbox_ops/notifications.js.map +2 -2
  162. package/dist/modules/messages/notifications.js +2 -0
  163. package/dist/modules/messages/notifications.js.map +2 -2
  164. package/dist/modules/notifications/acl.js +3 -1
  165. package/dist/modules/notifications/acl.js.map +2 -2
  166. package/dist/modules/notifications/api/admin/preferences/route.js +167 -0
  167. package/dist/modules/notifications/api/admin/preferences/route.js.map +7 -0
  168. package/dist/modules/notifications/api/channels/route.js +54 -0
  169. package/dist/modules/notifications/api/channels/route.js.map +7 -0
  170. package/dist/modules/notifications/api/preferences/route.js +126 -0
  171. package/dist/modules/notifications/api/preferences/route.js.map +7 -0
  172. package/dist/modules/notifications/api/route.js +8 -1
  173. package/dist/modules/notifications/api/route.js.map +2 -2
  174. package/dist/modules/notifications/api/types/[id]/channels/[channel]/route.js +187 -0
  175. package/dist/modules/notifications/api/types/[id]/channels/[channel]/route.js.map +7 -0
  176. package/dist/modules/notifications/api/types/route.js +225 -0
  177. package/dist/modules/notifications/api/types/route.js.map +7 -0
  178. package/dist/modules/notifications/api/unread-count/route.js +5 -1
  179. package/dist/modules/notifications/api/unread-count/route.js.map +2 -2
  180. package/dist/modules/notifications/backend/notifications/user-preferences/page.js +10 -0
  181. package/dist/modules/notifications/backend/notifications/user-preferences/page.js.map +7 -0
  182. package/dist/modules/notifications/backend/notifications/user-preferences/page.meta.js +28 -0
  183. package/dist/modules/notifications/backend/notifications/user-preferences/page.meta.js.map +7 -0
  184. package/dist/modules/notifications/backend/profile/notification-preferences/page.js +10 -0
  185. package/dist/modules/notifications/backend/profile/notification-preferences/page.js.map +7 -0
  186. package/dist/modules/notifications/backend/profile/notification-preferences/page.meta.js +33 -0
  187. package/dist/modules/notifications/backend/profile/notification-preferences/page.meta.js.map +7 -0
  188. package/dist/modules/notifications/data/entities.js +128 -1
  189. package/dist/modules/notifications/data/entities.js.map +2 -2
  190. package/dist/modules/notifications/data/validators.js +95 -2
  191. package/dist/modules/notifications/data/validators.js.map +2 -2
  192. package/dist/modules/notifications/di.js +7 -0
  193. package/dist/modules/notifications/di.js.map +2 -2
  194. package/dist/modules/notifications/events.js +17 -0
  195. package/dist/modules/notifications/events.js.map +2 -2
  196. package/dist/modules/notifications/frontend/NotificationPreferenceMatrix.js +190 -0
  197. package/dist/modules/notifications/frontend/NotificationPreferenceMatrix.js.map +7 -0
  198. package/dist/modules/notifications/frontend/NotificationPreferencesPageClient.js +124 -0
  199. package/dist/modules/notifications/frontend/NotificationPreferencesPageClient.js.map +7 -0
  200. package/dist/modules/notifications/frontend/NotificationSettingsPageClient.js +166 -1
  201. package/dist/modules/notifications/frontend/NotificationSettingsPageClient.js.map +2 -2
  202. package/dist/modules/notifications/frontend/NotificationUserPreferencesAdminPageClient.js +172 -0
  203. package/dist/modules/notifications/frontend/NotificationUserPreferencesAdminPageClient.js.map +7 -0
  204. package/dist/modules/notifications/generators.js +63 -0
  205. package/dist/modules/notifications/generators.js.map +7 -0
  206. package/dist/modules/notifications/lib/delivery-strategies-registry.js +19 -0
  207. package/dist/modules/notifications/lib/delivery-strategies-registry.js.map +7 -0
  208. package/dist/modules/notifications/lib/deliveryStrategies.js.map +2 -2
  209. package/dist/modules/notifications/lib/derive-category.js +8 -0
  210. package/dist/modules/notifications/lib/derive-category.js.map +7 -0
  211. package/dist/modules/notifications/lib/notification-channel-registry.js +35 -0
  212. package/dist/modules/notifications/lib/notification-channel-registry.js.map +7 -0
  213. package/dist/modules/notifications/lib/notification-type-registry.js +87 -0
  214. package/dist/modules/notifications/lib/notification-type-registry.js.map +7 -0
  215. package/dist/modules/notifications/lib/notificationCopy.js +14 -0
  216. package/dist/modules/notifications/lib/notificationCopy.js.map +7 -0
  217. package/dist/modules/notifications/lib/notificationFactory.js +4 -1
  218. package/dist/modules/notifications/lib/notificationFactory.js.map +2 -2
  219. package/dist/modules/notifications/lib/notificationMapper.js +2 -0
  220. package/dist/modules/notifications/lib/notificationMapper.js.map +2 -2
  221. package/dist/modules/notifications/lib/notificationPreferenceService.js +83 -0
  222. package/dist/modules/notifications/lib/notificationPreferenceService.js.map +7 -0
  223. package/dist/modules/notifications/lib/notificationService.js +83 -28
  224. package/dist/modules/notifications/lib/notificationService.js.map +2 -2
  225. package/dist/modules/notifications/lib/notificationVisibility.js +24 -0
  226. package/dist/modules/notifications/lib/notificationVisibility.js.map +7 -0
  227. package/dist/modules/notifications/lib/routeHelpers.js +11 -1
  228. package/dist/modules/notifications/lib/routeHelpers.js.map +2 -2
  229. package/dist/modules/notifications/lib/shouldDeliver.js +46 -0
  230. package/dist/modules/notifications/lib/shouldDeliver.js.map +7 -0
  231. package/dist/modules/notifications/lib/strategies/email-delivery-strategy.js +54 -0
  232. package/dist/modules/notifications/lib/strategies/email-delivery-strategy.js.map +7 -0
  233. package/dist/modules/notifications/lib/strategies/in-app-delivery-strategy.js +12 -0
  234. package/dist/modules/notifications/lib/strategies/in-app-delivery-strategy.js.map +7 -0
  235. package/dist/modules/notifications/lib/typeCatalogue.js +71 -0
  236. package/dist/modules/notifications/lib/typeCatalogue.js.map +7 -0
  237. package/dist/modules/notifications/lib/typeChannelSettings.js +12 -0
  238. package/dist/modules/notifications/lib/typeChannelSettings.js.map +7 -0
  239. package/dist/modules/notifications/lib/typeOverrides.js +19 -0
  240. package/dist/modules/notifications/lib/typeOverrides.js.map +7 -0
  241. package/dist/modules/notifications/migrations/Migration20260625122947_notifications.js +14 -0
  242. package/dist/modules/notifications/migrations/Migration20260625122947_notifications.js.map +7 -0
  243. package/dist/modules/notifications/migrations/Migration20260626120500_notifications_push_payload.js +15 -0
  244. package/dist/modules/notifications/migrations/Migration20260626120500_notifications_push_payload.js.map +7 -0
  245. package/dist/modules/notifications/migrations/Migration20260626130000_notifications_non_opt_out.js +13 -0
  246. package/dist/modules/notifications/migrations/Migration20260626130000_notifications_non_opt_out.js.map +7 -0
  247. package/dist/modules/notifications/migrations/Migration20260626140000_notifications_type_category_silent.js +15 -0
  248. package/dist/modules/notifications/migrations/Migration20260626140000_notifications_type_category_silent.js.map +7 -0
  249. package/dist/modules/notifications/migrations/Migration20260706120000_notifications_channels.js +13 -0
  250. package/dist/modules/notifications/migrations/Migration20260706120000_notifications_channels.js.map +7 -0
  251. package/dist/modules/notifications/migrations/Migration20260716123121_notifications.js +14 -0
  252. package/dist/modules/notifications/migrations/Migration20260716123121_notifications.js.map +7 -0
  253. package/dist/modules/notifications/notification-channels.js +26 -0
  254. package/dist/modules/notifications/notification-channels.js.map +7 -0
  255. package/dist/modules/notifications/notifications.delivery-strategies.js +12 -0
  256. package/dist/modules/notifications/notifications.delivery-strategies.js.map +7 -0
  257. package/dist/modules/notifications/setup.js +16 -1
  258. package/dist/modules/notifications/setup.js.map +2 -2
  259. package/dist/modules/notifications/subscribers/deliver-notification.js +66 -57
  260. package/dist/modules/notifications/subscribers/deliver-notification.js.map +2 -2
  261. package/dist/modules/notifications/subscribers/sync-notification-types.js +16 -0
  262. package/dist/modules/notifications/subscribers/sync-notification-types.js.map +7 -0
  263. package/dist/modules/notifications/widgets/injection/profile-preferences-menu/widget.js +23 -0
  264. package/dist/modules/notifications/widgets/injection/profile-preferences-menu/widget.js.map +7 -0
  265. package/dist/modules/notifications/widgets/injection-table.js +14 -0
  266. package/dist/modules/notifications/widgets/injection-table.js.map +7 -0
  267. package/dist/modules/push_notifications/acl.js +13 -0
  268. package/dist/modules/push_notifications/acl.js.map +7 -0
  269. package/dist/modules/push_notifications/api/custom-send/route.js +141 -0
  270. package/dist/modules/push_notifications/api/custom-send/route.js.map +7 -0
  271. package/dist/modules/push_notifications/api/deliveries/[id]/route.js +110 -0
  272. package/dist/modules/push_notifications/api/deliveries/[id]/route.js.map +7 -0
  273. package/dist/modules/push_notifications/api/deliveries/route.js +56 -0
  274. package/dist/modules/push_notifications/api/deliveries/route.js.map +7 -0
  275. package/dist/modules/push_notifications/api/openapi.js +19 -0
  276. package/dist/modules/push_notifications/api/openapi.js.map +7 -0
  277. package/dist/modules/push_notifications/backend/push_notifications/[id]/page.js +103 -0
  278. package/dist/modules/push_notifications/backend/push_notifications/[id]/page.js.map +7 -0
  279. package/dist/modules/push_notifications/backend/push_notifications/[id]/page.meta.js +13 -0
  280. package/dist/modules/push_notifications/backend/push_notifications/[id]/page.meta.js.map +7 -0
  281. package/dist/modules/push_notifications/backend/push_notifications/page.js +213 -0
  282. package/dist/modules/push_notifications/backend/push_notifications/page.js.map +7 -0
  283. package/dist/modules/push_notifications/backend/push_notifications/page.meta.js +23 -0
  284. package/dist/modules/push_notifications/backend/push_notifications/page.meta.js.map +7 -0
  285. package/dist/modules/push_notifications/backend/push_notifications/send/page.js +245 -0
  286. package/dist/modules/push_notifications/backend/push_notifications/send/page.js.map +7 -0
  287. package/dist/modules/push_notifications/backend/push_notifications/send/page.meta.js +26 -0
  288. package/dist/modules/push_notifications/backend/push_notifications/send/page.meta.js.map +7 -0
  289. package/dist/modules/push_notifications/data/entities.js +92 -0
  290. package/dist/modules/push_notifications/data/entities.js.map +7 -0
  291. package/dist/modules/push_notifications/data/extensions.js +23 -0
  292. package/dist/modules/push_notifications/data/extensions.js.map +7 -0
  293. package/dist/modules/push_notifications/data/validators.js +89 -0
  294. package/dist/modules/push_notifications/data/validators.js.map +7 -0
  295. package/dist/modules/push_notifications/di.js +17 -0
  296. package/dist/modules/push_notifications/di.js.map +7 -0
  297. package/dist/modules/push_notifications/events.js +24 -0
  298. package/dist/modules/push_notifications/events.js.map +7 -0
  299. package/dist/modules/push_notifications/index.js +16 -0
  300. package/dist/modules/push_notifications/index.js.map +7 -0
  301. package/dist/modules/push_notifications/lib/fake-provider-recorder.js +80 -0
  302. package/dist/modules/push_notifications/lib/fake-provider-recorder.js.map +7 -0
  303. package/dist/modules/push_notifications/lib/push-delivery-strategy.js +54 -0
  304. package/dist/modules/push_notifications/lib/push-delivery-strategy.js.map +7 -0
  305. package/dist/modules/push_notifications/lib/push-delivery.js +297 -0
  306. package/dist/modules/push_notifications/lib/push-delivery.js.map +7 -0
  307. package/dist/modules/push_notifications/lib/push-fanout.js +138 -0
  308. package/dist/modules/push_notifications/lib/push-fanout.js.map +7 -0
  309. package/dist/modules/push_notifications/lib/push-health.js +32 -0
  310. package/dist/modules/push_notifications/lib/push-health.js.map +7 -0
  311. package/dist/modules/push_notifications/lib/push-reaper.js +87 -0
  312. package/dist/modules/push_notifications/lib/push-reaper.js.map +7 -0
  313. package/dist/modules/push_notifications/lib/push-receipt-reaper.js +136 -0
  314. package/dist/modules/push_notifications/lib/push-receipt-reaper.js.map +7 -0
  315. package/dist/modules/push_notifications/lib/push-stub-adapter.js +63 -0
  316. package/dist/modules/push_notifications/lib/push-stub-adapter.js.map +7 -0
  317. package/dist/modules/push_notifications/lib/queue.js +53 -0
  318. package/dist/modules/push_notifications/lib/queue.js.map +7 -0
  319. package/dist/modules/push_notifications/lib/reclaim-window.js +13 -0
  320. package/dist/modules/push_notifications/lib/reclaim-window.js.map +7 -0
  321. package/dist/modules/push_notifications/lib/send-custom-push.js +48 -0
  322. package/dist/modules/push_notifications/lib/send-custom-push.js.map +7 -0
  323. package/dist/modules/push_notifications/migrations/Migration20260625150049_push_notifications.js +16 -0
  324. package/dist/modules/push_notifications/migrations/Migration20260625150049_push_notifications.js.map +7 -0
  325. package/dist/modules/push_notifications/migrations/Migration20260626120000_push_notifications_silent.js +13 -0
  326. package/dist/modules/push_notifications/migrations/Migration20260626120000_push_notifications_silent.js.map +7 -0
  327. package/dist/modules/push_notifications/notifications.delivery-strategies.js +8 -0
  328. package/dist/modules/push_notifications/notifications.delivery-strategies.js.map +7 -0
  329. package/dist/modules/push_notifications/notifications.js +40 -0
  330. package/dist/modules/push_notifications/notifications.js.map +7 -0
  331. package/dist/modules/push_notifications/setup.js +52 -0
  332. package/dist/modules/push_notifications/setup.js.map +7 -0
  333. package/dist/modules/push_notifications/workers/reclaim-stuck.worker.js +57 -0
  334. package/dist/modules/push_notifications/workers/reclaim-stuck.worker.js.map +7 -0
  335. package/dist/modules/push_notifications/workers/send-push.worker.js +27 -0
  336. package/dist/modules/push_notifications/workers/send-push.worker.js.map +7 -0
  337. package/dist/modules/sales/notifications.js +8 -0
  338. package/dist/modules/sales/notifications.js.map +2 -2
  339. package/dist/modules/staff/notifications.js +6 -0
  340. package/dist/modules/staff/notifications.js.map +2 -2
  341. package/dist/modules/workflows/notifications.js +2 -0
  342. package/dist/modules/workflows/notifications.js.map +2 -2
  343. package/generated/entities/notification/index.ts +3 -0
  344. package/generated/entities/notification_preference/index.ts +8 -0
  345. package/generated/entities/notification_type/index.ts +9 -0
  346. package/generated/entities/notification_type_override/index.ts +7 -0
  347. package/generated/entities/push_notification_delivery/index.ts +19 -0
  348. package/generated/entities/user_device/index.ts +16 -0
  349. package/generated/entities.ids.generated.ts +12 -1
  350. package/generated/entity-fields-registry.ts +72 -0
  351. package/package.json +7 -7
  352. package/src/helpers/integration/api.ts +2 -1
  353. package/src/helpers/integration/appRoot.ts +13 -0
  354. package/src/helpers/integration/dbFixtures.ts +1 -5
  355. package/src/helpers/integration/pushFake.ts +219 -0
  356. package/src/modules/auth/i18n/de.json +1 -0
  357. package/src/modules/auth/i18n/en.json +1 -0
  358. package/src/modules/auth/i18n/es.json +1 -0
  359. package/src/modules/auth/i18n/ko.json +1 -0
  360. package/src/modules/auth/i18n/pl.json +1 -0
  361. package/src/modules/auth/notifications.ts +16 -0
  362. package/src/modules/business_rules/i18n/de.json +2 -1
  363. package/src/modules/business_rules/i18n/en.json +2 -1
  364. package/src/modules/business_rules/i18n/es.json +2 -1
  365. package/src/modules/business_rules/i18n/ko.json +2 -1
  366. package/src/modules/business_rules/i18n/pl.json +2 -1
  367. package/src/modules/business_rules/notifications.ts +2 -0
  368. package/src/modules/catalog/i18n/de.json +2 -1
  369. package/src/modules/catalog/i18n/en.json +2 -1
  370. package/src/modules/catalog/i18n/es.json +2 -1
  371. package/src/modules/catalog/i18n/ko.json +2 -1
  372. package/src/modules/catalog/i18n/pl.json +2 -1
  373. package/src/modules/catalog/notifications.ts +2 -0
  374. package/src/modules/communication_channels/acl.ts +9 -0
  375. package/src/modules/communication_channels/api/delete/admin/channels/[id]/route.ts +168 -0
  376. package/src/modules/communication_channels/api/get/channels/[id]/health/route.ts +6 -2
  377. package/src/modules/communication_channels/api/get/channels/[id]/route.ts +6 -2
  378. package/src/modules/communication_channels/api/get/channels/route.ts +2 -1
  379. package/src/modules/communication_channels/api/post/channels/[id]/test-send/route.ts +16 -5
  380. package/src/modules/communication_channels/api/post/channels/connect/credentials/route.ts +37 -1
  381. package/src/modules/communication_channels/api/post/channels/connect/tenant-credentials/route.ts +175 -0
  382. package/src/modules/communication_channels/backend/communication_channels/channels/page.tsx +11 -1
  383. package/src/modules/communication_channels/commands/admin-delete-channel.ts +203 -0
  384. package/src/modules/communication_channels/commands/connect-credential-channel.ts +63 -15
  385. package/src/modules/communication_channels/commands/deliver-outbound-message.ts +10 -2
  386. package/src/modules/communication_channels/commands/push-register.ts +7 -1
  387. package/src/modules/communication_channels/commands/push-unregister.ts +14 -3
  388. package/src/modules/communication_channels/data/entities.ts +10 -0
  389. package/src/modules/communication_channels/data/validators.ts +1 -0
  390. package/src/modules/communication_channels/i18n/de.json +35 -1
  391. package/src/modules/communication_channels/i18n/en.json +35 -1
  392. package/src/modules/communication_channels/i18n/es.json +35 -1
  393. package/src/modules/communication_channels/i18n/ko.json +35 -1
  394. package/src/modules/communication_channels/i18n/pl.json +35 -1
  395. package/src/modules/communication_channels/lib/access-control.ts +42 -0
  396. package/src/modules/communication_channels/lib/adapter.ts +25 -0
  397. package/src/modules/communication_channels/lib/connect-channel.ts +61 -26
  398. package/src/modules/communication_channels/lib/push-adapter.ts +139 -0
  399. package/src/modules/communication_channels/lib/push-capabilities.ts +42 -0
  400. package/src/modules/communication_channels/lib/push-connect-error.ts +52 -0
  401. package/src/modules/communication_channels/lib/push-credential-errors.ts +58 -0
  402. package/src/modules/communication_channels/lib/push-envelope.ts +104 -0
  403. package/src/modules/communication_channels/lib/refcounted-client-cache.ts +123 -0
  404. package/src/modules/communication_channels/migrations/.snapshot-open-mercato.json +9 -0
  405. package/src/modules/communication_channels/migrations/Migration20260703123630_communication_channels.ts +20 -0
  406. package/src/modules/communication_channels/notifications.ts +4 -0
  407. package/src/modules/communication_channels/setup.ts +2 -0
  408. package/src/modules/configs/i18n/de.json +5 -1
  409. package/src/modules/configs/i18n/en.json +5 -1
  410. package/src/modules/configs/i18n/es.json +5 -1
  411. package/src/modules/configs/i18n/ko.json +5 -1
  412. package/src/modules/configs/i18n/pl.json +5 -1
  413. package/src/modules/configs/lib/upgrade-actions.ts +21 -0
  414. package/src/modules/customer_accounts/i18n/de.json +2 -1
  415. package/src/modules/customer_accounts/i18n/en.json +2 -1
  416. package/src/modules/customer_accounts/i18n/es.json +2 -1
  417. package/src/modules/customer_accounts/i18n/ko.json +2 -1
  418. package/src/modules/customer_accounts/i18n/pl.json +2 -1
  419. package/src/modules/customer_accounts/notifications.ts +12 -0
  420. package/src/modules/customers/i18n/de.json +2 -1
  421. package/src/modules/customers/i18n/en.json +2 -1
  422. package/src/modules/customers/i18n/es.json +2 -1
  423. package/src/modules/customers/i18n/ko.json +2 -1
  424. package/src/modules/customers/i18n/pl.json +2 -1
  425. package/src/modules/customers/notifications.ts +4 -0
  426. package/src/modules/devices/AGENTS.md +101 -0
  427. package/src/modules/devices/acl.ts +7 -0
  428. package/src/modules/devices/api/[id]/route.ts +180 -0
  429. package/src/modules/devices/api/admin/devices/[id]/route.ts +235 -0
  430. package/src/modules/devices/api/admin/devices/route.ts +160 -0
  431. package/src/modules/devices/api/auth.ts +13 -0
  432. package/src/modules/devices/api/deviceList.ts +54 -0
  433. package/src/modules/devices/api/deviceOps.ts +186 -0
  434. package/src/modules/devices/api/openapi.ts +23 -0
  435. package/src/modules/devices/api/route.ts +165 -0
  436. package/src/modules/devices/backend/devices/[id]/page.meta.ts +12 -0
  437. package/src/modules/devices/backend/devices/[id]/page.tsx +164 -0
  438. package/src/modules/devices/backend/devices/create/page.meta.ts +12 -0
  439. package/src/modules/devices/backend/devices/create/page.tsx +81 -0
  440. package/src/modules/devices/backend/devices/page.meta.ts +21 -0
  441. package/src/modules/devices/backend/devices/page.tsx +259 -0
  442. package/src/modules/devices/commands/deactivate.ts +126 -0
  443. package/src/modules/devices/commands/index.ts +3 -0
  444. package/src/modules/devices/commands/register.ts +199 -0
  445. package/src/modules/devices/commands/shared.ts +119 -0
  446. package/src/modules/devices/commands/update.ts +142 -0
  447. package/src/modules/devices/data/entities.ts +79 -0
  448. package/src/modules/devices/data/validators.ts +65 -0
  449. package/src/modules/devices/di.ts +9 -0
  450. package/src/modules/devices/encryption.ts +15 -0
  451. package/src/modules/devices/events.ts +16 -0
  452. package/src/modules/devices/i18n/de.json +45 -0
  453. package/src/modules/devices/i18n/en.json +45 -0
  454. package/src/modules/devices/i18n/es.json +45 -0
  455. package/src/modules/devices/i18n/ko.json +45 -0
  456. package/src/modules/devices/i18n/pl.json +45 -0
  457. package/src/modules/devices/index.ts +15 -0
  458. package/src/modules/devices/lib/operationMetadata.ts +35 -0
  459. package/src/modules/devices/migrations/.snapshot-open-mercato.json +200 -0
  460. package/src/modules/devices/migrations/Migration20260602120000.ts +15 -0
  461. package/src/modules/devices/migrations/Migration20260630120000.ts +20 -0
  462. package/src/modules/devices/migrations/Migration20260701120000.ts +13 -0
  463. package/src/modules/devices/migrations/Migration20260721120000.ts +18 -0
  464. package/src/modules/devices/setup.ts +11 -0
  465. package/src/modules/entities/cli.ts +16 -2
  466. package/src/modules/entities/migrations/Migration20260722120000.ts +38 -0
  467. package/src/modules/inbox_ops/i18n/de.json +2 -1
  468. package/src/modules/inbox_ops/i18n/en.json +2 -1
  469. package/src/modules/inbox_ops/i18n/es.json +2 -1
  470. package/src/modules/inbox_ops/i18n/ko.json +2 -1
  471. package/src/modules/inbox_ops/i18n/pl.json +2 -1
  472. package/src/modules/inbox_ops/notifications.ts +2 -0
  473. package/src/modules/messages/i18n/de.json +2 -1
  474. package/src/modules/messages/i18n/en.json +2 -1
  475. package/src/modules/messages/i18n/es.json +2 -1
  476. package/src/modules/messages/i18n/ko.json +2 -1
  477. package/src/modules/messages/i18n/pl.json +2 -1
  478. package/src/modules/messages/notifications.ts +2 -0
  479. package/src/modules/notifications/AGENTS.md +54 -0
  480. package/src/modules/notifications/acl.ts +2 -0
  481. package/src/modules/notifications/api/admin/preferences/route.ts +193 -0
  482. package/src/modules/notifications/api/channels/route.ts +54 -0
  483. package/src/modules/notifications/api/preferences/route.ts +131 -0
  484. package/src/modules/notifications/api/route.ts +8 -1
  485. package/src/modules/notifications/api/types/[id]/channels/[channel]/route.ts +226 -0
  486. package/src/modules/notifications/api/types/route.ts +239 -0
  487. package/src/modules/notifications/api/unread-count/route.ts +5 -1
  488. package/src/modules/notifications/backend/notifications/user-preferences/page.meta.ts +26 -0
  489. package/src/modules/notifications/backend/notifications/user-preferences/page.tsx +12 -0
  490. package/src/modules/notifications/backend/profile/notification-preferences/page.meta.ts +31 -0
  491. package/src/modules/notifications/backend/profile/notification-preferences/page.tsx +12 -0
  492. package/src/modules/notifications/data/entities.ts +144 -0
  493. package/src/modules/notifications/data/validators.ts +116 -0
  494. package/src/modules/notifications/di.ts +7 -0
  495. package/src/modules/notifications/events.ts +23 -0
  496. package/src/modules/notifications/frontend/NotificationPreferenceMatrix.tsx +287 -0
  497. package/src/modules/notifications/frontend/NotificationPreferencesPageClient.tsx +155 -0
  498. package/src/modules/notifications/frontend/NotificationSettingsPageClient.tsx +273 -2
  499. package/src/modules/notifications/frontend/NotificationUserPreferencesAdminPageClient.tsx +214 -0
  500. package/src/modules/notifications/generators.ts +82 -0
  501. package/src/modules/notifications/i18n/de.json +43 -0
  502. package/src/modules/notifications/i18n/en.json +43 -0
  503. package/src/modules/notifications/i18n/es.json +43 -0
  504. package/src/modules/notifications/i18n/ko.json +43 -0
  505. package/src/modules/notifications/i18n/pl.json +44 -1
  506. package/src/modules/notifications/lib/delivery-strategies-registry.ts +30 -0
  507. package/src/modules/notifications/lib/deliveryStrategies.ts +40 -4
  508. package/src/modules/notifications/lib/derive-category.ts +15 -0
  509. package/src/modules/notifications/lib/notification-channel-registry.ts +59 -0
  510. package/src/modules/notifications/lib/notification-type-registry.ts +182 -0
  511. package/src/modules/notifications/lib/notificationCopy.ts +34 -0
  512. package/src/modules/notifications/lib/notificationFactory.ts +5 -1
  513. package/src/modules/notifications/lib/notificationMapper.ts +2 -0
  514. package/src/modules/notifications/lib/notificationPreferenceService.ts +138 -0
  515. package/src/modules/notifications/lib/notificationService.ts +129 -25
  516. package/src/modules/notifications/lib/notificationVisibility.ts +48 -0
  517. package/src/modules/notifications/lib/routeHelpers.ts +14 -1
  518. package/src/modules/notifications/lib/shouldDeliver.ts +112 -0
  519. package/src/modules/notifications/lib/strategies/email-delivery-strategy.ts +63 -0
  520. package/src/modules/notifications/lib/strategies/in-app-delivery-strategy.ts +21 -0
  521. package/src/modules/notifications/lib/typeCatalogue.ts +141 -0
  522. package/src/modules/notifications/lib/typeChannelSettings.ts +29 -0
  523. package/src/modules/notifications/lib/typeOverrides.ts +46 -0
  524. package/src/modules/notifications/migrations/.snapshot-open-mercato.json +874 -100
  525. package/src/modules/notifications/migrations/Migration20260625122947_notifications.ts +14 -0
  526. package/src/modules/notifications/migrations/Migration20260626120500_notifications_push_payload.ts +15 -0
  527. package/src/modules/notifications/migrations/Migration20260626130000_notifications_non_opt_out.ts +13 -0
  528. package/src/modules/notifications/migrations/Migration20260626140000_notifications_type_category_silent.ts +15 -0
  529. package/src/modules/notifications/migrations/Migration20260706120000_notifications_channels.ts +13 -0
  530. package/src/modules/notifications/migrations/Migration20260716123121_notifications.ts +14 -0
  531. package/src/modules/notifications/notification-channels.ts +30 -0
  532. package/src/modules/notifications/notifications.delivery-strategies.ts +15 -0
  533. package/src/modules/notifications/setup.ts +23 -1
  534. package/src/modules/notifications/subscribers/deliver-notification.ts +105 -71
  535. package/src/modules/notifications/subscribers/sync-notification-types.ts +18 -0
  536. package/src/modules/notifications/widgets/injection/profile-preferences-menu/widget.ts +35 -0
  537. package/src/modules/notifications/widgets/injection-table.ts +18 -0
  538. package/src/modules/push_notifications/AGENTS.md +92 -0
  539. package/src/modules/push_notifications/acl.ts +9 -0
  540. package/src/modules/push_notifications/api/custom-send/route.ts +175 -0
  541. package/src/modules/push_notifications/api/deliveries/[id]/route.ts +122 -0
  542. package/src/modules/push_notifications/api/deliveries/route.ts +58 -0
  543. package/src/modules/push_notifications/api/openapi.ts +22 -0
  544. package/src/modules/push_notifications/backend/push_notifications/[id]/page.meta.ts +9 -0
  545. package/src/modules/push_notifications/backend/push_notifications/[id]/page.tsx +149 -0
  546. package/src/modules/push_notifications/backend/push_notifications/page.meta.ts +21 -0
  547. package/src/modules/push_notifications/backend/push_notifications/page.tsx +239 -0
  548. package/src/modules/push_notifications/backend/push_notifications/send/page.meta.ts +24 -0
  549. package/src/modules/push_notifications/backend/push_notifications/send/page.tsx +327 -0
  550. package/src/modules/push_notifications/data/entities.ts +111 -0
  551. package/src/modules/push_notifications/data/extensions.ts +29 -0
  552. package/src/modules/push_notifications/data/validators.ts +102 -0
  553. package/src/modules/push_notifications/di.ts +20 -0
  554. package/src/modules/push_notifications/events.ts +21 -0
  555. package/src/modules/push_notifications/i18n/de.json +79 -0
  556. package/src/modules/push_notifications/i18n/en.json +79 -0
  557. package/src/modules/push_notifications/i18n/es.json +79 -0
  558. package/src/modules/push_notifications/i18n/ko.json +79 -0
  559. package/src/modules/push_notifications/i18n/pl.json +79 -0
  560. package/src/modules/push_notifications/index.ts +15 -0
  561. package/src/modules/push_notifications/lib/fake-provider-recorder.ts +154 -0
  562. package/src/modules/push_notifications/lib/push-delivery-strategy.ts +90 -0
  563. package/src/modules/push_notifications/lib/push-delivery.ts +455 -0
  564. package/src/modules/push_notifications/lib/push-fanout.ts +237 -0
  565. package/src/modules/push_notifications/lib/push-health.ts +47 -0
  566. package/src/modules/push_notifications/lib/push-reaper.ts +140 -0
  567. package/src/modules/push_notifications/lib/push-receipt-reaper.ts +210 -0
  568. package/src/modules/push_notifications/lib/push-stub-adapter.ts +80 -0
  569. package/src/modules/push_notifications/lib/queue.ts +74 -0
  570. package/src/modules/push_notifications/lib/reclaim-window.ts +16 -0
  571. package/src/modules/push_notifications/lib/send-custom-push.ts +100 -0
  572. package/src/modules/push_notifications/migrations/.snapshot-open-mercato.json +386 -0
  573. package/src/modules/push_notifications/migrations/Migration20260625150049_push_notifications.ts +16 -0
  574. package/src/modules/push_notifications/migrations/Migration20260626120000_push_notifications_silent.ts +13 -0
  575. package/src/modules/push_notifications/notifications.delivery-strategies.ts +8 -0
  576. package/src/modules/push_notifications/notifications.ts +50 -0
  577. package/src/modules/push_notifications/setup.ts +92 -0
  578. package/src/modules/push_notifications/workers/reclaim-stuck.worker.ts +78 -0
  579. package/src/modules/push_notifications/workers/send-push.worker.ts +34 -0
  580. package/src/modules/sales/i18n/de.json +1 -0
  581. package/src/modules/sales/i18n/en.json +1 -0
  582. package/src/modules/sales/i18n/es.json +1 -0
  583. package/src/modules/sales/i18n/ko.json +1 -0
  584. package/src/modules/sales/i18n/pl.json +1 -0
  585. package/src/modules/sales/notifications.ts +8 -0
  586. package/src/modules/staff/i18n/de.json +1 -0
  587. package/src/modules/staff/i18n/en.json +1 -0
  588. package/src/modules/staff/i18n/es.json +1 -0
  589. package/src/modules/staff/i18n/ko.json +1 -0
  590. package/src/modules/staff/i18n/pl.json +1 -0
  591. package/src/modules/staff/notifications.ts +6 -0
  592. package/src/modules/workflows/i18n/de.json +1 -0
  593. package/src/modules/workflows/i18n/en.json +1 -0
  594. package/src/modules/workflows/i18n/es.json +1 -0
  595. package/src/modules/workflows/i18n/ko.json +1 -0
  596. package/src/modules/workflows/i18n/pl.json +1 -0
  597. package/src/modules/workflows/notifications.ts +2 -0
@@ -0,0 +1,160 @@
1
+ import type { EntityManager } from '@mikro-orm/postgresql'
2
+ import { NextResponse } from 'next/server'
3
+ import { z } from 'zod'
4
+ import { makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'
5
+ import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
6
+ import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
7
+ import { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'
8
+ import { isOrganizationReadAccessAllowed } from '@open-mercato/core/modules/directory/utils/organizationScopeGuard'
9
+ import { User } from '@open-mercato/core/modules/auth/data/entities'
10
+ import { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'
11
+ import { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'
12
+ import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
13
+ import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
14
+ import { createLogger } from '@open-mercato/shared/lib/logger'
15
+ import { E } from '#generated/entities.ids.generated'
16
+ import { UserDevice } from '../../../data/entities'
17
+ import {
18
+ registerDeviceAdminSchema,
19
+ registerDeviceCommandSchema,
20
+ type RegisterDeviceCommandInput,
21
+ } from '../../../data/validators'
22
+ import { resolveDeviceActorUserId } from '../../auth'
23
+ import { createDevicesCrudOpenApi, createPagedListResponseSchema } from '../../openapi'
24
+ import { deviceListSchema, deviceListFields, deviceListSortFieldMap, deviceListItemSchema } from '../../deviceList'
25
+ import { executeRegister, type DeviceMutationContext } from '../../deviceOps'
26
+
27
+ const logger = createLogger('devices')
28
+
29
+ // Admin device administration: cross-user listing + register-on-behalf-of-user. Gated by devices.admin.
30
+ // Self-serve registration/listing stays on /api/devices (scoped to the acting user).
31
+ const routeMetadata = {
32
+ GET: { requireAuth: true, requireFeatures: ['devices.admin'] },
33
+ POST: { requireAuth: true, requireFeatures: ['devices.admin'] },
34
+ }
35
+
36
+ export const metadata = routeMetadata
37
+
38
+ const crud = makeCrudRoute({
39
+ metadata: routeMetadata,
40
+ orm: {
41
+ entity: UserDevice,
42
+ idField: 'id',
43
+ // organization_id stays nullable, but scoping follows the standard org-scoped pattern: the factory
44
+ // auto-filters by the caller's organization scope (null-aware in the query engine). Unrestricted
45
+ // admins see the whole tenant including null-org rows; org-restricted admins see only their orgs.
46
+ orgField: 'organizationId',
47
+ tenantField: 'tenantId',
48
+ softDeleteField: 'deletedAt',
49
+ },
50
+ indexer: { entityType: E.devices.user_device },
51
+ // Same cache resource tag as the self route + commands, so any device write busts both list caches.
52
+ events: { module: 'devices', entity: 'user_device' },
53
+ list: {
54
+ schema: deviceListSchema,
55
+ entityId: E.devices.user_device,
56
+ fields: deviceListFields,
57
+ sortFieldMap: deviceListSortFieldMap,
58
+ // Tenant + org scope is enforced by the factory (orm.tenantField + orm.orgField); only the optional
59
+ // userId/platform narrowing is left to do here.
60
+ buildFilters: async (query) => {
61
+ const filters: Record<string, unknown> = {}
62
+ if (query.userId) filters.user_id = { $eq: query.userId }
63
+ if (query.platform) filters.platform = { $eq: query.platform }
64
+ return filters
65
+ },
66
+ },
67
+ })
68
+
69
+ export const GET = crud.GET
70
+
71
+ export async function POST(req: Request) {
72
+ const { translate } = await resolveTranslations()
73
+ try {
74
+ const container = await createRequestContainer()
75
+ const auth = await getAuthFromRequest(req)
76
+ const actorUserId = resolveDeviceActorUserId(auth)
77
+ if (!auth || !auth.tenantId || !actorUserId) {
78
+ return NextResponse.json({ error: translate('devices.errors.unauthorized', 'Unauthorized') }, { status: 401 })
79
+ }
80
+
81
+ const body = registerDeviceAdminSchema.parse(await readJsonSafe(req, {}))
82
+ const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })
83
+ const organizationId = scope?.selectedId ?? auth.orgId ?? null
84
+
85
+ // Validate the on-behalf-of target before registering. `body.userId` is caller-supplied, and the
86
+ // fan-out delivers by `(tenant, org, user)` — so without this an admin could register their OWN push
87
+ // token under an arbitrary `userId` (even a non-existent or cross-tenant one) and start receiving that
88
+ // user's notifications. Require a real, active user in the admin's tenant who is also within the
89
+ // admin's organization scope (org-restricted admins cannot reach users in orgs they can't access).
90
+ // Only non-encrypted columns are selected, so no decryption is needed.
91
+ const em = container.resolve('em') as EntityManager
92
+ const targetUser = await em.findOne(
93
+ User,
94
+ { id: body.userId, tenantId: auth.tenantId, deletedAt: null },
95
+ { fields: ['id', 'organizationId'] },
96
+ )
97
+ if (!targetUser) {
98
+ return NextResponse.json(
99
+ { error: translate('devices.errors.user_not_found', 'Target user not found in this tenant') },
100
+ { status: 400 },
101
+ )
102
+ }
103
+ if (!isOrganizationReadAccessAllowed({ scope, auth, organizationId: targetUser.organizationId ?? null })) {
104
+ return NextResponse.json({ error: translate('devices.errors.forbidden', 'Access denied') }, { status: 403 })
105
+ }
106
+
107
+ // Register is an idempotent upsert keyed per (tenant, org, user, device). organizationId is the
108
+ // admin's own resolved scope, so the upsert can only ever touch a row inside an org the admin can
109
+ // access — no cross-org guard is needed here.
110
+ const commandInput = registerDeviceCommandSchema.parse({
111
+ ...body,
112
+ tenantId: auth.tenantId,
113
+ organizationId,
114
+ userId: body.userId,
115
+ } satisfies RegisterDeviceCommandInput)
116
+
117
+ // actorUserId is the admin (for the mutation guard/audit); the device is owned by body.userId.
118
+ const mctx: DeviceMutationContext = { container, auth, scope, organizationId, actorUserId, request: req }
119
+ return await executeRegister(mctx, commandInput)
120
+ } catch (err) {
121
+ if (err instanceof z.ZodError) {
122
+ return NextResponse.json(
123
+ { error: translate('devices.errors.invalid_payload', 'Invalid device payload'), details: err.flatten() },
124
+ { status: 400 },
125
+ )
126
+ }
127
+ if (isCrudHttpError(err) && (err.body as { code?: string } | undefined)?.code === 'device_already_registered') {
128
+ return NextResponse.json(
129
+ { error: translate('devices.errors.already_registered', 'This device is already registered') },
130
+ { status: 409 },
131
+ )
132
+ }
133
+ if (isCrudHttpError(err)) {
134
+ return NextResponse.json(err.body, { status: err.status })
135
+ }
136
+ logger.error('devices.admin.POST failed', { err })
137
+ return NextResponse.json(
138
+ { error: translate('devices.errors.register_failed', 'Failed to register device') },
139
+ { status: 500 },
140
+ )
141
+ }
142
+ }
143
+
144
+ const registerResponseSchema = z.object({
145
+ id: z.string().uuid(),
146
+ deviceId: z.string(),
147
+ revived: z.boolean(),
148
+ })
149
+
150
+ export const openApi: OpenApiRouteDoc = createDevicesCrudOpenApi({
151
+ resourceName: 'Device (admin)',
152
+ pluralName: 'Devices (admin)',
153
+ querySchema: deviceListSchema,
154
+ listResponseSchema: createPagedListResponseSchema(deviceListItemSchema),
155
+ create: {
156
+ schema: registerDeviceAdminSchema,
157
+ responseSchema: registerResponseSchema,
158
+ description: 'Admin: register (idempotently upsert) a device on behalf of any user in the tenant.',
159
+ },
160
+ })
@@ -0,0 +1,13 @@
1
+ import type { AuthContext } from '@open-mercato/shared/lib/auth/server'
2
+
3
+ /**
4
+ * Resolves the acting user id for device routes. Device ownership is per-user, so API-key
5
+ * principals (which have no human user) are not valid device actors and yield null. Mirrors the
6
+ * `auth.isApiKey ? null : auth.sub` convention used across the codebase; the target user id is
7
+ * validated as a UUID by the command schemas downstream.
8
+ */
9
+ export function resolveDeviceActorUserId(auth: AuthContext | null): string | null {
10
+ if (!auth || auth.isApiKey) return null
11
+ const subjectId = typeof auth.sub === 'string' ? auth.sub.trim() : ''
12
+ return subjectId || null
13
+ }
@@ -0,0 +1,54 @@
1
+ import { z } from 'zod'
2
+
3
+ // Shared list contract for the self-serve (`/api/devices`) and admin (`/api/devices/admin/devices`)
4
+ // list routes. push_token is a secret and is never part of the exposed field set.
5
+ export const deviceListSchema = z
6
+ .object({
7
+ page: z.coerce.number().min(1).default(1),
8
+ pageSize: z.coerce.number().min(1).max(100).default(50),
9
+ platform: z.enum(['ios', 'android', 'web']).optional(),
10
+ userId: z.string().uuid().optional(),
11
+ sortField: z.string().optional(),
12
+ sortDir: z.enum(['asc', 'desc']).optional(),
13
+ })
14
+ .passthrough()
15
+
16
+ export const deviceListFields: string[] = [
17
+ 'id',
18
+ 'tenant_id',
19
+ 'organization_id',
20
+ 'user_id',
21
+ 'device_id',
22
+ 'platform',
23
+ 'client_app_version',
24
+ 'os_version',
25
+ 'locale',
26
+ 'push_provider',
27
+ 'push_token_updated_at',
28
+ 'last_seen_at',
29
+ 'created_at',
30
+ 'updated_at',
31
+ ]
32
+
33
+ export const deviceListSortFieldMap: Record<string, string> = {
34
+ lastSeenAt: 'last_seen_at',
35
+ createdAt: 'created_at',
36
+ updatedAt: 'updated_at',
37
+ }
38
+
39
+ export const deviceListItemSchema = z.object({
40
+ id: z.string().uuid(),
41
+ tenant_id: z.string().uuid(),
42
+ organization_id: z.string().uuid().nullable().optional(),
43
+ user_id: z.string().uuid(),
44
+ device_id: z.string(),
45
+ platform: z.enum(['ios', 'android', 'web']),
46
+ client_app_version: z.string().nullable().optional(),
47
+ os_version: z.string().nullable().optional(),
48
+ locale: z.string().nullable().optional(),
49
+ push_provider: z.string().nullable().optional(),
50
+ push_token_updated_at: z.string().nullable().optional(),
51
+ last_seen_at: z.string().nullable().optional(),
52
+ created_at: z.string().nullable().optional(),
53
+ updated_at: z.string().nullable().optional(),
54
+ })
@@ -0,0 +1,186 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
3
+ import type { AuthContext } from '@open-mercato/shared/lib/auth/server'
4
+ import { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'
5
+ import {
6
+ runCrudMutationGuardAfterSuccess,
7
+ validateCrudMutationGuard,
8
+ } from '@open-mercato/shared/lib/crud/mutation-guard'
9
+ import { enforceCommandOptimisticLock } from '@open-mercato/shared/lib/crud/optimistic-lock-command'
10
+ import type { CommandBus } from '@open-mercato/shared/lib/commands'
11
+ import type { UserDevice } from '../data/entities'
12
+ import type {
13
+ RegisterDeviceCommandInput,
14
+ UpdateDeviceCommandInput,
15
+ DeactivateDeviceCommandInput,
16
+ } from '../data/validators'
17
+ import { attachOperationMetadataHeader } from '../lib/operationMetadata'
18
+
19
+ const RESOURCE_KIND = 'devices.user_device'
20
+
21
+ // push_token is a secret. The mutation guard forwards mutationPayload into record-lock conflict
22
+ // details that are returned to clients, so strip the token before it reaches the guard.
23
+ export function redactMutationPayload(
24
+ payload: Record<string, unknown> | undefined,
25
+ ): Record<string, unknown> | undefined {
26
+ if (!payload || !('pushToken' in payload)) return payload
27
+ const { pushToken: _pushToken, ...rest } = payload
28
+ return rest
29
+ }
30
+
31
+ type RequestContainer = Awaited<ReturnType<typeof createRequestContainer>>
32
+ type OrganizationScope = Awaited<ReturnType<typeof resolveOrganizationScopeForRequest>>
33
+
34
+ // Shared mutation context for device write ops. `actorUserId` is the authenticated caller (used for
35
+ // the mutation guard / audit), independent of which user *owns* the device being mutated.
36
+ export type DeviceMutationContext = {
37
+ container: RequestContainer
38
+ auth: NonNullable<AuthContext>
39
+ scope: OrganizationScope
40
+ organizationId: string | null
41
+ actorUserId: string
42
+ request: Request
43
+ }
44
+
45
+ function commandCtx(mctx: DeviceMutationContext) {
46
+ return {
47
+ container: mctx.container,
48
+ auth: mctx.auth,
49
+ organizationScope: mctx.scope ?? null,
50
+ selectedOrganizationId: mctx.organizationId,
51
+ organizationIds: mctx.scope?.filterIds ?? (mctx.auth.orgId ? [mctx.auth.orgId] : null),
52
+ request: mctx.request,
53
+ }
54
+ }
55
+
56
+ async function runGuards(
57
+ mctx: DeviceMutationContext,
58
+ operation: 'create' | 'update' | 'delete',
59
+ resourceId: string,
60
+ mutationPayload?: Record<string, unknown>,
61
+ ) {
62
+ return validateCrudMutationGuard(mctx.container, {
63
+ tenantId: mctx.auth.tenantId!,
64
+ organizationId: mctx.organizationId,
65
+ userId: mctx.actorUserId,
66
+ resourceKind: RESOURCE_KIND,
67
+ resourceId,
68
+ operation,
69
+ requestMethod: mctx.request.method,
70
+ requestHeaders: mctx.request.headers,
71
+ mutationPayload,
72
+ })
73
+ }
74
+
75
+ async function runAfter(
76
+ mctx: DeviceMutationContext,
77
+ guardResult: Awaited<ReturnType<typeof validateCrudMutationGuard>>,
78
+ operation: 'create' | 'update' | 'delete',
79
+ resourceId: string,
80
+ ) {
81
+ if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {
82
+ await runCrudMutationGuardAfterSuccess(mctx.container, {
83
+ tenantId: mctx.auth.tenantId!,
84
+ organizationId: mctx.organizationId,
85
+ userId: mctx.actorUserId,
86
+ resourceKind: RESOURCE_KIND,
87
+ resourceId,
88
+ operation,
89
+ requestMethod: mctx.request.method,
90
+ requestHeaders: mctx.request.headers,
91
+ metadata: (guardResult.metadata as Record<string, unknown> | null | undefined) ?? null,
92
+ })
93
+ }
94
+ }
95
+
96
+ export async function executeRegister(
97
+ mctx: DeviceMutationContext,
98
+ commandInput: RegisterDeviceCommandInput,
99
+ ): Promise<NextResponse> {
100
+ // Register is an upsert, so the device row id isn't known until execution. Key the mutation guard
101
+ // by the stable, tenant-unique upsert tuple so validate and after-success agree (record locks are
102
+ // keyed by resourceId); deviceId alone is not unique across users.
103
+ const guardResourceId = `${commandInput.userId}:${commandInput.deviceId}`
104
+ const guardResult = await runGuards(mctx, 'create', guardResourceId, redactMutationPayload(commandInput))
105
+ if (guardResult && !guardResult.ok) return NextResponse.json(guardResult.body, { status: guardResult.status })
106
+
107
+ const commandBus = mctx.container.resolve('commandBus') as CommandBus
108
+ const { result, logEntry } = await commandBus.execute<
109
+ RegisterDeviceCommandInput,
110
+ { id: string; deviceId: string; revived: boolean }
111
+ >('devices.user_devices.register', { input: commandInput, ctx: commandCtx(mctx) })
112
+
113
+ await runAfter(mctx, guardResult, 'create', guardResourceId)
114
+
115
+ const response = NextResponse.json(
116
+ { id: result.id, deviceId: result.deviceId, revived: result.revived },
117
+ { status: 201 },
118
+ )
119
+ attachOperationMetadataHeader(response, logEntry, { resourceKind: RESOURCE_KIND, resourceId: result.id })
120
+ return response
121
+ }
122
+
123
+ export async function executeUpdate(
124
+ mctx: DeviceMutationContext,
125
+ device: UserDevice,
126
+ body: Omit<UpdateDeviceCommandInput, 'id' | 'tenantId' | 'userId' | 'organizationId'>,
127
+ ): Promise<NextResponse> {
128
+ // Device metadata edits carry lost-update risk (unlike the idempotent soft-delete deactivate), so
129
+ // enforce OSS optimistic locking when the caller sends the expected-version header. No-ops when the
130
+ // header is absent (e.g. mobile self-update clients), keeping the contract backward-compatible.
131
+ enforceCommandOptimisticLock({
132
+ resourceKind: RESOURCE_KIND,
133
+ resourceId: device.id,
134
+ current: device.updatedAt,
135
+ request: mctx.request,
136
+ })
137
+
138
+ const guardResult = await runGuards(mctx, 'update', device.id, redactMutationPayload(body))
139
+ if (guardResult && !guardResult.ok) return NextResponse.json(guardResult.body, { status: guardResult.status })
140
+
141
+ const commandBus = mctx.container.resolve('commandBus') as CommandBus
142
+ const { logEntry } = await commandBus.execute<UpdateDeviceCommandInput, { id: string }>('devices.user_devices.update', {
143
+ input: {
144
+ ...body,
145
+ id: device.id,
146
+ tenantId: device.tenantId,
147
+ userId: device.userId,
148
+ organizationId: device.organizationId ?? null,
149
+ } satisfies UpdateDeviceCommandInput,
150
+ ctx: commandCtx(mctx),
151
+ })
152
+
153
+ await runAfter(mctx, guardResult, 'update', device.id)
154
+
155
+ const response = NextResponse.json({ ok: true, id: device.id })
156
+ attachOperationMetadataHeader(response, logEntry, { resourceKind: RESOURCE_KIND, resourceId: device.id })
157
+ return response
158
+ }
159
+
160
+ export async function executeDeactivate(
161
+ mctx: DeviceMutationContext,
162
+ device: UserDevice,
163
+ ): Promise<NextResponse> {
164
+ const guardResult = await runGuards(mctx, 'delete', device.id)
165
+ if (guardResult && !guardResult.ok) return NextResponse.json(guardResult.body, { status: guardResult.status })
166
+
167
+ const commandBus = mctx.container.resolve('commandBus') as CommandBus
168
+ const { logEntry } = await commandBus.execute<DeactivateDeviceCommandInput, { id: string }>(
169
+ 'devices.user_devices.deactivate',
170
+ {
171
+ input: {
172
+ id: device.id,
173
+ tenantId: device.tenantId,
174
+ userId: device.userId,
175
+ organizationId: device.organizationId ?? null,
176
+ } satisfies DeactivateDeviceCommandInput,
177
+ ctx: commandCtx(mctx),
178
+ },
179
+ )
180
+
181
+ await runAfter(mctx, guardResult, 'delete', device.id)
182
+
183
+ const response = NextResponse.json({ ok: true })
184
+ attachOperationMetadataHeader(response, logEntry, { resourceKind: RESOURCE_KIND, resourceId: device.id })
185
+ return response
186
+ }
@@ -0,0 +1,23 @@
1
+ import { type ZodTypeAny } from 'zod'
2
+ import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
3
+ import {
4
+ createCrudOpenApiFactory,
5
+ createPagedListResponseSchema as createSharedPagedListResponseSchema,
6
+ type CrudOpenApiOptions,
7
+ } from '@open-mercato/shared/lib/openapi/crud'
8
+
9
+ export function createPagedListResponseSchema(itemSchema: ZodTypeAny) {
10
+ return createSharedPagedListResponseSchema(itemSchema, { paginationMetaOptional: true })
11
+ }
12
+
13
+ // createCrudOpenApiFactory already falls back to the shared default create/ok response schemas
14
+ // when omitted, so there is nothing module-specific to re-pass or re-export here.
15
+ const buildDevicesCrudOpenApi = createCrudOpenApiFactory({
16
+ defaultTag: 'Devices',
17
+ makeListDescription: ({ pluralLower }) =>
18
+ `Returns the authenticated user's registered ${pluralLower} (admins may list across users).`,
19
+ })
20
+
21
+ export function createDevicesCrudOpenApi(options: CrudOpenApiOptions): OpenApiRouteDoc {
22
+ return buildDevicesCrudOpenApi(options)
23
+ }
@@ -0,0 +1,165 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { z } from 'zod'
3
+ import { makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'
4
+ import { consumeAdvancedFilterState, mergeAdvancedFilterTree } from '@open-mercato/shared/lib/crud/advanced-filter-integration'
5
+ import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
6
+ import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
7
+ import { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'
8
+ import { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'
9
+ import { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'
10
+ import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
11
+ import { resolveLocaleFromAcceptLanguage } from '@open-mercato/shared/lib/i18n/locale'
12
+ import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
13
+ import { createLogger } from '@open-mercato/shared/lib/logger'
14
+ import { E } from '#generated/entities.ids.generated'
15
+ import { UserDevice } from '../data/entities'
16
+ import {
17
+ registerDeviceSchema,
18
+ registerDeviceCommandSchema,
19
+ type RegisterDeviceCommandInput,
20
+ } from '../data/validators'
21
+ import { resolveDeviceActorUserId } from './auth'
22
+ import { createDevicesCrudOpenApi, createPagedListResponseSchema } from './openapi'
23
+ import { deviceListSchema, deviceListFields, deviceListSortFieldMap, deviceListItemSchema } from './deviceList'
24
+ import { executeRegister, type DeviceMutationContext } from './deviceOps'
25
+
26
+ const logger = createLogger('devices')
27
+
28
+ const routeMetadata = {
29
+ GET: { requireAuth: true, requireFeatures: ['devices.view'] },
30
+ POST: { requireAuth: true, requireFeatures: ['devices.manage'] },
31
+ }
32
+
33
+ export const metadata = routeMetadata
34
+
35
+ const NO_MATCH_USER_ID = '00000000-0000-0000-0000-000000000000'
36
+
37
+ const crud = makeCrudRoute({
38
+ metadata: routeMetadata,
39
+ orm: {
40
+ entity: UserDevice,
41
+ idField: 'id',
42
+ // organization_id stays nullable, but scoping follows the standard org-scoped pattern (like every
43
+ // other module): the factory auto-filters by the caller's organization scope. The query engine's
44
+ // org scope is null-aware — callers whose scope includes null (or who have no org restriction) also
45
+ // see null-org rows; a caller pinned to a specific org does not.
46
+ orgField: 'organizationId',
47
+ tenantField: 'tenantId',
48
+ softDeleteField: 'deletedAt',
49
+ },
50
+ indexer: { entityType: E.devices.user_device },
51
+ // Align the CRUD cache resource tag with the command's resourceKind ('devices.user_device') so
52
+ // writes through the device commands invalidate this list cache. Without this, the factory falls
53
+ // back to the ORM entity name and the cache is never busted (stale list under ENABLE_CRUD_API_CACHE).
54
+ // The factory does not own the write methods here (custom POST + command bus), so no events are emitted.
55
+ events: { module: 'devices', entity: 'user_device' },
56
+ list: {
57
+ schema: deviceListSchema,
58
+ entityId: E.devices.user_device,
59
+ // push_token is a secret and is never exposed via the list API.
60
+ fields: deviceListFields,
61
+ sortFieldMap: deviceListSortFieldMap,
62
+ // Exports are off for the self-serve list. The factory enables them for every list that does not
63
+ // declare `export` (resolveAvailableExportFormats falls back to csv/json/xml/markdown), and its
64
+ // full-export branch replaces the filters with `{}` instead of calling `buildFilters`
65
+ // (`baseFilters = exportFullRequested ? {} : buildFilters(...)`). On this route `buildFilters` is
66
+ // the only user-level boundary — automatic scoping covers tenant and organization, not the acting
67
+ // user — so `?format=json&exportScope=full` would return every device in the caller's org scope.
68
+ // With no available formats, `exportRequested` is never true and that branch is unreachable. Bulk
69
+ // device data belongs to the admin tree (api/admin/devices), which gates cross-user listing.
70
+ export: { enabled: false },
71
+ // Self-serve: always scoped to the acting user. Cross-user listing lives in api/admin/devices.
72
+ buildFilters: async (query, ctx) => {
73
+ // Consume the client-supplied advanced filter here (this deletes every `filter[...]` key from
74
+ // `query`) so the CRUD factory's own merge step becomes a no-op. That step is
75
+ // `{ ...existingFilters, ...advancedWhere }` (advanced-filter-integration.ts), and condition
76
+ // field names are never validated — `filter[conditions][0][field]=user_id` or `=$and` would
77
+ // otherwise overwrite whatever key this route uses for its scope and widen the listing to other
78
+ // users. `mergeAdvancedFilterTree` instead AND-combines under `$and: [routeFilters, treeWhere]`,
79
+ // where the scope sits in its own array element and no client key can reach it. Same pattern as
80
+ // customers/people, companies and deals.
81
+ const advancedFilterTree = consumeAdvancedFilterState(query)
82
+ const filters: Record<string, unknown> = {}
83
+ const actorUserId = resolveDeviceActorUserId(ctx.auth)
84
+ // Self-serve visibility is strictly the acting user's own devices; cross-user listing lives in
85
+ // api/admin/devices. Fail closed when there is no resolvable acting user (e.g. API keys).
86
+ filters.user_id = { $eq: actorUserId ?? NO_MATCH_USER_ID }
87
+ if (query.platform) filters.platform = { $eq: query.platform }
88
+ return mergeAdvancedFilterTree(filters, advancedFilterTree)
89
+ },
90
+ },
91
+ })
92
+
93
+ export const GET = crud.GET
94
+
95
+ export async function POST(req: Request) {
96
+ const { translate } = await resolveTranslations()
97
+ try {
98
+ const container = await createRequestContainer()
99
+ const auth = await getAuthFromRequest(req)
100
+ const actorUserId = resolveDeviceActorUserId(auth)
101
+ if (!auth || !auth.tenantId || !actorUserId) {
102
+ return NextResponse.json({ error: translate('devices.errors.unauthorized', 'Unauthorized') }, { status: 401 })
103
+ }
104
+
105
+ const body = registerDeviceSchema.parse(await readJsonSafe(req, {}))
106
+ const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })
107
+ const organizationId = scope?.selectedId ?? auth.orgId ?? null
108
+
109
+ // When the app doesn't send an explicit per-device locale, fall back to the request's
110
+ // Accept-Language (the mobile client calls this with its own headers). Only on self-register —
111
+ // PUT stays tri-state, and admin register uses the admin's browser headers, not the device's.
112
+ const headerLocale = resolveLocaleFromAcceptLanguage(req.headers.get('accept-language'))
113
+
114
+ const commandInput = registerDeviceCommandSchema.parse({
115
+ ...body,
116
+ locale: body.locale ?? headerLocale ?? undefined,
117
+ tenantId: auth.tenantId,
118
+ organizationId,
119
+ userId: actorUserId,
120
+ } satisfies RegisterDeviceCommandInput)
121
+
122
+ const mctx: DeviceMutationContext = { container, auth, scope, organizationId, actorUserId, request: req }
123
+ return await executeRegister(mctx, commandInput)
124
+ } catch (err) {
125
+ if (err instanceof z.ZodError) {
126
+ return NextResponse.json(
127
+ { error: translate('devices.errors.invalid_payload', 'Invalid device payload'), details: err.flatten() },
128
+ { status: 400 },
129
+ )
130
+ }
131
+ if (isCrudHttpError(err) && (err.body as { code?: string } | undefined)?.code === 'device_already_registered') {
132
+ return NextResponse.json(
133
+ { error: translate('devices.errors.already_registered', 'This device is already registered') },
134
+ { status: 409 },
135
+ )
136
+ }
137
+ if (isCrudHttpError(err)) {
138
+ return NextResponse.json(err.body, { status: err.status })
139
+ }
140
+ logger.error('devices.POST failed', { err })
141
+ return NextResponse.json(
142
+ { error: translate('devices.errors.register_failed', 'Failed to register device') },
143
+ { status: 500 },
144
+ )
145
+ }
146
+ }
147
+
148
+ const registerResponseSchema = z.object({
149
+ id: z.string().uuid(),
150
+ deviceId: z.string(),
151
+ revived: z.boolean(),
152
+ })
153
+
154
+ export const openApi: OpenApiRouteDoc = createDevicesCrudOpenApi({
155
+ resourceName: 'Device',
156
+ pluralName: 'Devices',
157
+ querySchema: deviceListSchema,
158
+ listResponseSchema: createPagedListResponseSchema(deviceListItemSchema),
159
+ create: {
160
+ schema: registerDeviceSchema,
161
+ responseSchema: registerResponseSchema,
162
+ description:
163
+ 'Registers (or idempotently upserts) the current user\'s device for the given deviceId. May include an initial push token.',
164
+ },
165
+ })
@@ -0,0 +1,12 @@
1
+ export const metadata = {
2
+ requireAuth: true,
3
+ requireFeatures: ['devices.admin'],
4
+ pageTitle: 'Edit device',
5
+ pageTitleKey: 'devices.form.editTitle',
6
+ pageContext: 'settings' as const,
7
+ navHidden: true,
8
+ breadcrumb: [
9
+ { label: 'Devices', labelKey: 'devices.nav.devices', href: '/backend/devices' },
10
+ { label: 'Edit device', labelKey: 'devices.form.editTitle' },
11
+ ],
12
+ }