@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.
- package/.turbo/turbo-build.log +2 -2
- package/dist/generated/entities/notification/index.js +6 -0
- package/dist/generated/entities/notification/index.js.map +2 -2
- package/dist/generated/entities/notification_preference/index.js +19 -0
- package/dist/generated/entities/notification_preference/index.js.map +7 -0
- package/dist/generated/entities/notification_type/index.js +21 -0
- package/dist/generated/entities/notification_type/index.js.map +7 -0
- package/dist/generated/entities/notification_type_override/index.js +17 -0
- package/dist/generated/entities/notification_type_override/index.js.map +7 -0
- package/dist/generated/entities/push_notification_delivery/index.js +41 -0
- package/dist/generated/entities/push_notification_delivery/index.js.map +7 -0
- package/dist/generated/entities/user_device/index.js +35 -0
- package/dist/generated/entities/user_device/index.js.map +7 -0
- package/dist/generated/entities.ids.generated.js +12 -1
- package/dist/generated/entities.ids.generated.js.map +2 -2
- package/dist/generated/entity-fields-registry.js +72 -0
- package/dist/generated/entity-fields-registry.js.map +2 -2
- package/dist/helpers/integration/api.js +2 -1
- package/dist/helpers/integration/api.js.map +2 -2
- package/dist/helpers/integration/appRoot.js +9 -0
- package/dist/helpers/integration/appRoot.js.map +7 -0
- package/dist/helpers/integration/dbFixtures.js +1 -4
- package/dist/helpers/integration/dbFixtures.js.map +2 -2
- package/dist/helpers/integration/pushFake.js +126 -0
- package/dist/helpers/integration/pushFake.js.map +7 -0
- package/dist/modules/auth/notifications.js +16 -0
- package/dist/modules/auth/notifications.js.map +2 -2
- package/dist/modules/business_rules/notifications.js +2 -0
- package/dist/modules/business_rules/notifications.js.map +2 -2
- package/dist/modules/catalog/notifications.js +2 -0
- package/dist/modules/catalog/notifications.js.map +2 -2
- package/dist/modules/communication_channels/acl.js +9 -0
- package/dist/modules/communication_channels/acl.js.map +2 -2
- package/dist/modules/communication_channels/api/delete/admin/channels/[id]/route.js +133 -0
- package/dist/modules/communication_channels/api/delete/admin/channels/[id]/route.js.map +7 -0
- package/dist/modules/communication_channels/api/get/channels/[id]/health/route.js +6 -2
- package/dist/modules/communication_channels/api/get/channels/[id]/health/route.js.map +2 -2
- package/dist/modules/communication_channels/api/get/channels/[id]/route.js +6 -2
- package/dist/modules/communication_channels/api/get/channels/[id]/route.js.map +2 -2
- package/dist/modules/communication_channels/api/get/channels/route.js +2 -1
- package/dist/modules/communication_channels/api/get/channels/route.js.map +2 -2
- package/dist/modules/communication_channels/api/post/channels/[id]/test-send/route.js +10 -5
- package/dist/modules/communication_channels/api/post/channels/[id]/test-send/route.js.map +2 -2
- package/dist/modules/communication_channels/api/post/channels/connect/credentials/route.js +29 -1
- package/dist/modules/communication_channels/api/post/channels/connect/credentials/route.js.map +2 -2
- package/dist/modules/communication_channels/api/post/channels/connect/tenant-credentials/route.js +157 -0
- package/dist/modules/communication_channels/api/post/channels/connect/tenant-credentials/route.js.map +7 -0
- package/dist/modules/communication_channels/backend/communication_channels/channels/page.js +8 -1
- package/dist/modules/communication_channels/backend/communication_channels/channels/page.js.map +2 -2
- package/dist/modules/communication_channels/commands/admin-delete-channel.js +141 -0
- package/dist/modules/communication_channels/commands/admin-delete-channel.js.map +7 -0
- package/dist/modules/communication_channels/commands/connect-credential-channel.js +25 -8
- package/dist/modules/communication_channels/commands/connect-credential-channel.js.map +2 -2
- package/dist/modules/communication_channels/commands/deliver-outbound-message.js +2 -2
- package/dist/modules/communication_channels/commands/deliver-outbound-message.js.map +2 -2
- package/dist/modules/communication_channels/commands/push-register.js +1 -1
- package/dist/modules/communication_channels/commands/push-register.js.map +2 -2
- package/dist/modules/communication_channels/commands/push-unregister.js +4 -3
- package/dist/modules/communication_channels/commands/push-unregister.js.map +2 -2
- package/dist/modules/communication_channels/data/entities.js +4 -0
- package/dist/modules/communication_channels/data/entities.js.map +2 -2
- package/dist/modules/communication_channels/data/validators.js +2 -1
- package/dist/modules/communication_channels/data/validators.js.map +2 -2
- package/dist/modules/communication_channels/lib/access-control.js +11 -1
- package/dist/modules/communication_channels/lib/access-control.js.map +2 -2
- package/dist/modules/communication_channels/lib/connect-channel.js +20 -12
- package/dist/modules/communication_channels/lib/connect-channel.js.map +2 -2
- package/dist/modules/communication_channels/lib/push-adapter.js +68 -0
- package/dist/modules/communication_channels/lib/push-adapter.js.map +7 -0
- package/dist/modules/communication_channels/lib/push-capabilities.js +31 -0
- package/dist/modules/communication_channels/lib/push-capabilities.js.map +7 -0
- package/dist/modules/communication_channels/lib/push-connect-error.js +20 -0
- package/dist/modules/communication_channels/lib/push-connect-error.js.map +7 -0
- package/dist/modules/communication_channels/lib/push-credential-errors.js +41 -0
- package/dist/modules/communication_channels/lib/push-credential-errors.js.map +7 -0
- package/dist/modules/communication_channels/lib/push-envelope.js +46 -0
- package/dist/modules/communication_channels/lib/push-envelope.js.map +7 -0
- package/dist/modules/communication_channels/lib/refcounted-client-cache.js +64 -0
- package/dist/modules/communication_channels/lib/refcounted-client-cache.js.map +7 -0
- package/dist/modules/communication_channels/migrations/Migration20260703123630_communication_channels.js +13 -0
- package/dist/modules/communication_channels/migrations/Migration20260703123630_communication_channels.js.map +7 -0
- package/dist/modules/communication_channels/notifications.js +4 -0
- package/dist/modules/communication_channels/notifications.js.map +2 -2
- package/dist/modules/communication_channels/setup.js +2 -0
- package/dist/modules/communication_channels/setup.js.map +2 -2
- package/dist/modules/configs/lib/upgrade-actions.js +21 -0
- package/dist/modules/configs/lib/upgrade-actions.js.map +2 -2
- package/dist/modules/customer_accounts/notifications.js +12 -0
- package/dist/modules/customer_accounts/notifications.js.map +2 -2
- package/dist/modules/customers/notifications.js +4 -0
- package/dist/modules/customers/notifications.js.map +2 -2
- package/dist/modules/devices/acl.js +11 -0
- package/dist/modules/devices/acl.js.map +7 -0
- package/dist/modules/devices/api/[id]/route.js +167 -0
- package/dist/modules/devices/api/[id]/route.js.map +7 -0
- package/dist/modules/devices/api/admin/devices/[id]/route.js +219 -0
- package/dist/modules/devices/api/admin/devices/[id]/route.js.map +7 -0
- package/dist/modules/devices/api/admin/devices/route.js +140 -0
- package/dist/modules/devices/api/admin/devices/route.js.map +7 -0
- package/dist/modules/devices/api/auth.js +9 -0
- package/dist/modules/devices/api/auth.js.map +7 -0
- package/dist/modules/devices/api/deviceList.js +53 -0
- package/dist/modules/devices/api/deviceList.js.map +7 -0
- package/dist/modules/devices/api/deviceOps.js +118 -0
- package/dist/modules/devices/api/deviceOps.js.map +7 -0
- package/dist/modules/devices/api/openapi.js +19 -0
- package/dist/modules/devices/api/openapi.js.map +7 -0
- package/dist/modules/devices/api/route.js +143 -0
- package/dist/modules/devices/api/route.js.map +7 -0
- package/dist/modules/devices/backend/devices/[id]/page.js +135 -0
- package/dist/modules/devices/backend/devices/[id]/page.js.map +7 -0
- package/dist/modules/devices/backend/devices/[id]/page.meta.js +16 -0
- package/dist/modules/devices/backend/devices/[id]/page.meta.js.map +7 -0
- package/dist/modules/devices/backend/devices/create/page.js +68 -0
- package/dist/modules/devices/backend/devices/create/page.js.map +7 -0
- package/dist/modules/devices/backend/devices/create/page.meta.js +16 -0
- package/dist/modules/devices/backend/devices/create/page.meta.js.map +7 -0
- package/dist/modules/devices/backend/devices/page.js +235 -0
- package/dist/modules/devices/backend/devices/page.js.map +7 -0
- package/dist/modules/devices/backend/devices/page.meta.js +23 -0
- package/dist/modules/devices/backend/devices/page.meta.js.map +7 -0
- package/dist/modules/devices/commands/deactivate.js +107 -0
- package/dist/modules/devices/commands/deactivate.js.map +7 -0
- package/dist/modules/devices/commands/index.js +4 -0
- package/dist/modules/devices/commands/index.js.map +7 -0
- package/dist/modules/devices/commands/register.js +183 -0
- package/dist/modules/devices/commands/register.js.map +7 -0
- package/dist/modules/devices/commands/shared.js +74 -0
- package/dist/modules/devices/commands/shared.js.map +7 -0
- package/dist/modules/devices/commands/update.js +126 -0
- package/dist/modules/devices/commands/update.js.map +7 -0
- package/dist/modules/devices/data/entities.js +83 -0
- package/dist/modules/devices/data/entities.js.map +7 -0
- package/dist/modules/devices/data/validators.js +49 -0
- package/dist/modules/devices/data/validators.js.map +7 -0
- package/dist/modules/devices/di.js +11 -0
- package/dist/modules/devices/di.js.map +7 -0
- package/dist/modules/devices/encryption.js +12 -0
- package/dist/modules/devices/encryption.js.map +7 -0
- package/dist/modules/devices/events.js +19 -0
- package/dist/modules/devices/events.js.map +7 -0
- package/dist/modules/devices/index.js +17 -0
- package/dist/modules/devices/index.js.map +7 -0
- package/dist/modules/devices/lib/operationMetadata.js +20 -0
- package/dist/modules/devices/lib/operationMetadata.js.map +7 -0
- package/dist/modules/devices/migrations/Migration20260602120000.js +15 -0
- package/dist/modules/devices/migrations/Migration20260602120000.js.map +7 -0
- package/dist/modules/devices/migrations/Migration20260630120000.js +15 -0
- package/dist/modules/devices/migrations/Migration20260630120000.js.map +7 -0
- package/dist/modules/devices/migrations/Migration20260701120000.js +13 -0
- package/dist/modules/devices/migrations/Migration20260701120000.js.map +7 -0
- package/dist/modules/devices/migrations/Migration20260721120000.js +15 -0
- package/dist/modules/devices/migrations/Migration20260721120000.js.map +7 -0
- package/dist/modules/devices/setup.js +13 -0
- package/dist/modules/devices/setup.js.map +7 -0
- package/dist/modules/entities/cli.js +8 -3
- package/dist/modules/entities/cli.js.map +2 -2
- package/dist/modules/entities/migrations/Migration20260722120000.js +28 -0
- package/dist/modules/entities/migrations/Migration20260722120000.js.map +7 -0
- package/dist/modules/inbox_ops/notifications.js +2 -0
- package/dist/modules/inbox_ops/notifications.js.map +2 -2
- package/dist/modules/messages/notifications.js +2 -0
- package/dist/modules/messages/notifications.js.map +2 -2
- package/dist/modules/notifications/acl.js +3 -1
- package/dist/modules/notifications/acl.js.map +2 -2
- package/dist/modules/notifications/api/admin/preferences/route.js +167 -0
- package/dist/modules/notifications/api/admin/preferences/route.js.map +7 -0
- package/dist/modules/notifications/api/channels/route.js +54 -0
- package/dist/modules/notifications/api/channels/route.js.map +7 -0
- package/dist/modules/notifications/api/preferences/route.js +126 -0
- package/dist/modules/notifications/api/preferences/route.js.map +7 -0
- package/dist/modules/notifications/api/route.js +8 -1
- package/dist/modules/notifications/api/route.js.map +2 -2
- package/dist/modules/notifications/api/types/[id]/channels/[channel]/route.js +187 -0
- package/dist/modules/notifications/api/types/[id]/channels/[channel]/route.js.map +7 -0
- package/dist/modules/notifications/api/types/route.js +225 -0
- package/dist/modules/notifications/api/types/route.js.map +7 -0
- package/dist/modules/notifications/api/unread-count/route.js +5 -1
- package/dist/modules/notifications/api/unread-count/route.js.map +2 -2
- package/dist/modules/notifications/backend/notifications/user-preferences/page.js +10 -0
- package/dist/modules/notifications/backend/notifications/user-preferences/page.js.map +7 -0
- package/dist/modules/notifications/backend/notifications/user-preferences/page.meta.js +28 -0
- package/dist/modules/notifications/backend/notifications/user-preferences/page.meta.js.map +7 -0
- package/dist/modules/notifications/backend/profile/notification-preferences/page.js +10 -0
- package/dist/modules/notifications/backend/profile/notification-preferences/page.js.map +7 -0
- package/dist/modules/notifications/backend/profile/notification-preferences/page.meta.js +33 -0
- package/dist/modules/notifications/backend/profile/notification-preferences/page.meta.js.map +7 -0
- package/dist/modules/notifications/data/entities.js +128 -1
- package/dist/modules/notifications/data/entities.js.map +2 -2
- package/dist/modules/notifications/data/validators.js +95 -2
- package/dist/modules/notifications/data/validators.js.map +2 -2
- package/dist/modules/notifications/di.js +7 -0
- package/dist/modules/notifications/di.js.map +2 -2
- package/dist/modules/notifications/events.js +17 -0
- package/dist/modules/notifications/events.js.map +2 -2
- package/dist/modules/notifications/frontend/NotificationPreferenceMatrix.js +190 -0
- package/dist/modules/notifications/frontend/NotificationPreferenceMatrix.js.map +7 -0
- package/dist/modules/notifications/frontend/NotificationPreferencesPageClient.js +124 -0
- package/dist/modules/notifications/frontend/NotificationPreferencesPageClient.js.map +7 -0
- package/dist/modules/notifications/frontend/NotificationSettingsPageClient.js +166 -1
- package/dist/modules/notifications/frontend/NotificationSettingsPageClient.js.map +2 -2
- package/dist/modules/notifications/frontend/NotificationUserPreferencesAdminPageClient.js +172 -0
- package/dist/modules/notifications/frontend/NotificationUserPreferencesAdminPageClient.js.map +7 -0
- package/dist/modules/notifications/generators.js +63 -0
- package/dist/modules/notifications/generators.js.map +7 -0
- package/dist/modules/notifications/lib/delivery-strategies-registry.js +19 -0
- package/dist/modules/notifications/lib/delivery-strategies-registry.js.map +7 -0
- package/dist/modules/notifications/lib/deliveryStrategies.js.map +2 -2
- package/dist/modules/notifications/lib/derive-category.js +8 -0
- package/dist/modules/notifications/lib/derive-category.js.map +7 -0
- package/dist/modules/notifications/lib/notification-channel-registry.js +35 -0
- package/dist/modules/notifications/lib/notification-channel-registry.js.map +7 -0
- package/dist/modules/notifications/lib/notification-type-registry.js +87 -0
- package/dist/modules/notifications/lib/notification-type-registry.js.map +7 -0
- package/dist/modules/notifications/lib/notificationCopy.js +14 -0
- package/dist/modules/notifications/lib/notificationCopy.js.map +7 -0
- package/dist/modules/notifications/lib/notificationFactory.js +4 -1
- package/dist/modules/notifications/lib/notificationFactory.js.map +2 -2
- package/dist/modules/notifications/lib/notificationMapper.js +2 -0
- package/dist/modules/notifications/lib/notificationMapper.js.map +2 -2
- package/dist/modules/notifications/lib/notificationPreferenceService.js +83 -0
- package/dist/modules/notifications/lib/notificationPreferenceService.js.map +7 -0
- package/dist/modules/notifications/lib/notificationService.js +83 -28
- package/dist/modules/notifications/lib/notificationService.js.map +2 -2
- package/dist/modules/notifications/lib/notificationVisibility.js +24 -0
- package/dist/modules/notifications/lib/notificationVisibility.js.map +7 -0
- package/dist/modules/notifications/lib/routeHelpers.js +11 -1
- package/dist/modules/notifications/lib/routeHelpers.js.map +2 -2
- package/dist/modules/notifications/lib/shouldDeliver.js +46 -0
- package/dist/modules/notifications/lib/shouldDeliver.js.map +7 -0
- package/dist/modules/notifications/lib/strategies/email-delivery-strategy.js +54 -0
- package/dist/modules/notifications/lib/strategies/email-delivery-strategy.js.map +7 -0
- package/dist/modules/notifications/lib/strategies/in-app-delivery-strategy.js +12 -0
- package/dist/modules/notifications/lib/strategies/in-app-delivery-strategy.js.map +7 -0
- package/dist/modules/notifications/lib/typeCatalogue.js +71 -0
- package/dist/modules/notifications/lib/typeCatalogue.js.map +7 -0
- package/dist/modules/notifications/lib/typeChannelSettings.js +12 -0
- package/dist/modules/notifications/lib/typeChannelSettings.js.map +7 -0
- package/dist/modules/notifications/lib/typeOverrides.js +19 -0
- package/dist/modules/notifications/lib/typeOverrides.js.map +7 -0
- package/dist/modules/notifications/migrations/Migration20260625122947_notifications.js +14 -0
- package/dist/modules/notifications/migrations/Migration20260625122947_notifications.js.map +7 -0
- package/dist/modules/notifications/migrations/Migration20260626120500_notifications_push_payload.js +15 -0
- package/dist/modules/notifications/migrations/Migration20260626120500_notifications_push_payload.js.map +7 -0
- package/dist/modules/notifications/migrations/Migration20260626130000_notifications_non_opt_out.js +13 -0
- package/dist/modules/notifications/migrations/Migration20260626130000_notifications_non_opt_out.js.map +7 -0
- package/dist/modules/notifications/migrations/Migration20260626140000_notifications_type_category_silent.js +15 -0
- package/dist/modules/notifications/migrations/Migration20260626140000_notifications_type_category_silent.js.map +7 -0
- package/dist/modules/notifications/migrations/Migration20260706120000_notifications_channels.js +13 -0
- package/dist/modules/notifications/migrations/Migration20260706120000_notifications_channels.js.map +7 -0
- package/dist/modules/notifications/migrations/Migration20260716123121_notifications.js +14 -0
- package/dist/modules/notifications/migrations/Migration20260716123121_notifications.js.map +7 -0
- package/dist/modules/notifications/notification-channels.js +26 -0
- package/dist/modules/notifications/notification-channels.js.map +7 -0
- package/dist/modules/notifications/notifications.delivery-strategies.js +12 -0
- package/dist/modules/notifications/notifications.delivery-strategies.js.map +7 -0
- package/dist/modules/notifications/setup.js +16 -1
- package/dist/modules/notifications/setup.js.map +2 -2
- package/dist/modules/notifications/subscribers/deliver-notification.js +66 -57
- package/dist/modules/notifications/subscribers/deliver-notification.js.map +2 -2
- package/dist/modules/notifications/subscribers/sync-notification-types.js +16 -0
- package/dist/modules/notifications/subscribers/sync-notification-types.js.map +7 -0
- package/dist/modules/notifications/widgets/injection/profile-preferences-menu/widget.js +23 -0
- package/dist/modules/notifications/widgets/injection/profile-preferences-menu/widget.js.map +7 -0
- package/dist/modules/notifications/widgets/injection-table.js +14 -0
- package/dist/modules/notifications/widgets/injection-table.js.map +7 -0
- package/dist/modules/push_notifications/acl.js +13 -0
- package/dist/modules/push_notifications/acl.js.map +7 -0
- package/dist/modules/push_notifications/api/custom-send/route.js +141 -0
- package/dist/modules/push_notifications/api/custom-send/route.js.map +7 -0
- package/dist/modules/push_notifications/api/deliveries/[id]/route.js +110 -0
- package/dist/modules/push_notifications/api/deliveries/[id]/route.js.map +7 -0
- package/dist/modules/push_notifications/api/deliveries/route.js +56 -0
- package/dist/modules/push_notifications/api/deliveries/route.js.map +7 -0
- package/dist/modules/push_notifications/api/openapi.js +19 -0
- package/dist/modules/push_notifications/api/openapi.js.map +7 -0
- package/dist/modules/push_notifications/backend/push_notifications/[id]/page.js +103 -0
- package/dist/modules/push_notifications/backend/push_notifications/[id]/page.js.map +7 -0
- package/dist/modules/push_notifications/backend/push_notifications/[id]/page.meta.js +13 -0
- package/dist/modules/push_notifications/backend/push_notifications/[id]/page.meta.js.map +7 -0
- package/dist/modules/push_notifications/backend/push_notifications/page.js +213 -0
- package/dist/modules/push_notifications/backend/push_notifications/page.js.map +7 -0
- package/dist/modules/push_notifications/backend/push_notifications/page.meta.js +23 -0
- package/dist/modules/push_notifications/backend/push_notifications/page.meta.js.map +7 -0
- package/dist/modules/push_notifications/backend/push_notifications/send/page.js +245 -0
- package/dist/modules/push_notifications/backend/push_notifications/send/page.js.map +7 -0
- package/dist/modules/push_notifications/backend/push_notifications/send/page.meta.js +26 -0
- package/dist/modules/push_notifications/backend/push_notifications/send/page.meta.js.map +7 -0
- package/dist/modules/push_notifications/data/entities.js +92 -0
- package/dist/modules/push_notifications/data/entities.js.map +7 -0
- package/dist/modules/push_notifications/data/extensions.js +23 -0
- package/dist/modules/push_notifications/data/extensions.js.map +7 -0
- package/dist/modules/push_notifications/data/validators.js +89 -0
- package/dist/modules/push_notifications/data/validators.js.map +7 -0
- package/dist/modules/push_notifications/di.js +17 -0
- package/dist/modules/push_notifications/di.js.map +7 -0
- package/dist/modules/push_notifications/events.js +24 -0
- package/dist/modules/push_notifications/events.js.map +7 -0
- package/dist/modules/push_notifications/index.js +16 -0
- package/dist/modules/push_notifications/index.js.map +7 -0
- package/dist/modules/push_notifications/lib/fake-provider-recorder.js +80 -0
- package/dist/modules/push_notifications/lib/fake-provider-recorder.js.map +7 -0
- package/dist/modules/push_notifications/lib/push-delivery-strategy.js +54 -0
- package/dist/modules/push_notifications/lib/push-delivery-strategy.js.map +7 -0
- package/dist/modules/push_notifications/lib/push-delivery.js +297 -0
- package/dist/modules/push_notifications/lib/push-delivery.js.map +7 -0
- package/dist/modules/push_notifications/lib/push-fanout.js +138 -0
- package/dist/modules/push_notifications/lib/push-fanout.js.map +7 -0
- package/dist/modules/push_notifications/lib/push-health.js +32 -0
- package/dist/modules/push_notifications/lib/push-health.js.map +7 -0
- package/dist/modules/push_notifications/lib/push-reaper.js +87 -0
- package/dist/modules/push_notifications/lib/push-reaper.js.map +7 -0
- package/dist/modules/push_notifications/lib/push-receipt-reaper.js +136 -0
- package/dist/modules/push_notifications/lib/push-receipt-reaper.js.map +7 -0
- package/dist/modules/push_notifications/lib/push-stub-adapter.js +63 -0
- package/dist/modules/push_notifications/lib/push-stub-adapter.js.map +7 -0
- package/dist/modules/push_notifications/lib/queue.js +53 -0
- package/dist/modules/push_notifications/lib/queue.js.map +7 -0
- package/dist/modules/push_notifications/lib/reclaim-window.js +13 -0
- package/dist/modules/push_notifications/lib/reclaim-window.js.map +7 -0
- package/dist/modules/push_notifications/lib/send-custom-push.js +48 -0
- package/dist/modules/push_notifications/lib/send-custom-push.js.map +7 -0
- package/dist/modules/push_notifications/migrations/Migration20260625150049_push_notifications.js +16 -0
- package/dist/modules/push_notifications/migrations/Migration20260625150049_push_notifications.js.map +7 -0
- package/dist/modules/push_notifications/migrations/Migration20260626120000_push_notifications_silent.js +13 -0
- package/dist/modules/push_notifications/migrations/Migration20260626120000_push_notifications_silent.js.map +7 -0
- package/dist/modules/push_notifications/notifications.delivery-strategies.js +8 -0
- package/dist/modules/push_notifications/notifications.delivery-strategies.js.map +7 -0
- package/dist/modules/push_notifications/notifications.js +40 -0
- package/dist/modules/push_notifications/notifications.js.map +7 -0
- package/dist/modules/push_notifications/setup.js +52 -0
- package/dist/modules/push_notifications/setup.js.map +7 -0
- package/dist/modules/push_notifications/workers/reclaim-stuck.worker.js +57 -0
- package/dist/modules/push_notifications/workers/reclaim-stuck.worker.js.map +7 -0
- package/dist/modules/push_notifications/workers/send-push.worker.js +27 -0
- package/dist/modules/push_notifications/workers/send-push.worker.js.map +7 -0
- package/dist/modules/sales/notifications.js +8 -0
- package/dist/modules/sales/notifications.js.map +2 -2
- package/dist/modules/staff/notifications.js +6 -0
- package/dist/modules/staff/notifications.js.map +2 -2
- package/dist/modules/workflows/notifications.js +2 -0
- package/dist/modules/workflows/notifications.js.map +2 -2
- package/generated/entities/notification/index.ts +3 -0
- package/generated/entities/notification_preference/index.ts +8 -0
- package/generated/entities/notification_type/index.ts +9 -0
- package/generated/entities/notification_type_override/index.ts +7 -0
- package/generated/entities/push_notification_delivery/index.ts +19 -0
- package/generated/entities/user_device/index.ts +16 -0
- package/generated/entities.ids.generated.ts +12 -1
- package/generated/entity-fields-registry.ts +72 -0
- package/package.json +7 -7
- package/src/helpers/integration/api.ts +2 -1
- package/src/helpers/integration/appRoot.ts +13 -0
- package/src/helpers/integration/dbFixtures.ts +1 -5
- package/src/helpers/integration/pushFake.ts +219 -0
- package/src/modules/auth/i18n/de.json +1 -0
- package/src/modules/auth/i18n/en.json +1 -0
- package/src/modules/auth/i18n/es.json +1 -0
- package/src/modules/auth/i18n/ko.json +1 -0
- package/src/modules/auth/i18n/pl.json +1 -0
- package/src/modules/auth/notifications.ts +16 -0
- package/src/modules/business_rules/i18n/de.json +2 -1
- package/src/modules/business_rules/i18n/en.json +2 -1
- package/src/modules/business_rules/i18n/es.json +2 -1
- package/src/modules/business_rules/i18n/ko.json +2 -1
- package/src/modules/business_rules/i18n/pl.json +2 -1
- package/src/modules/business_rules/notifications.ts +2 -0
- package/src/modules/catalog/i18n/de.json +2 -1
- package/src/modules/catalog/i18n/en.json +2 -1
- package/src/modules/catalog/i18n/es.json +2 -1
- package/src/modules/catalog/i18n/ko.json +2 -1
- package/src/modules/catalog/i18n/pl.json +2 -1
- package/src/modules/catalog/notifications.ts +2 -0
- package/src/modules/communication_channels/acl.ts +9 -0
- package/src/modules/communication_channels/api/delete/admin/channels/[id]/route.ts +168 -0
- package/src/modules/communication_channels/api/get/channels/[id]/health/route.ts +6 -2
- package/src/modules/communication_channels/api/get/channels/[id]/route.ts +6 -2
- package/src/modules/communication_channels/api/get/channels/route.ts +2 -1
- package/src/modules/communication_channels/api/post/channels/[id]/test-send/route.ts +16 -5
- package/src/modules/communication_channels/api/post/channels/connect/credentials/route.ts +37 -1
- package/src/modules/communication_channels/api/post/channels/connect/tenant-credentials/route.ts +175 -0
- package/src/modules/communication_channels/backend/communication_channels/channels/page.tsx +11 -1
- package/src/modules/communication_channels/commands/admin-delete-channel.ts +203 -0
- package/src/modules/communication_channels/commands/connect-credential-channel.ts +63 -15
- package/src/modules/communication_channels/commands/deliver-outbound-message.ts +10 -2
- package/src/modules/communication_channels/commands/push-register.ts +7 -1
- package/src/modules/communication_channels/commands/push-unregister.ts +14 -3
- package/src/modules/communication_channels/data/entities.ts +10 -0
- package/src/modules/communication_channels/data/validators.ts +1 -0
- package/src/modules/communication_channels/i18n/de.json +35 -1
- package/src/modules/communication_channels/i18n/en.json +35 -1
- package/src/modules/communication_channels/i18n/es.json +35 -1
- package/src/modules/communication_channels/i18n/ko.json +35 -1
- package/src/modules/communication_channels/i18n/pl.json +35 -1
- package/src/modules/communication_channels/lib/access-control.ts +42 -0
- package/src/modules/communication_channels/lib/adapter.ts +25 -0
- package/src/modules/communication_channels/lib/connect-channel.ts +61 -26
- package/src/modules/communication_channels/lib/push-adapter.ts +139 -0
- package/src/modules/communication_channels/lib/push-capabilities.ts +42 -0
- package/src/modules/communication_channels/lib/push-connect-error.ts +52 -0
- package/src/modules/communication_channels/lib/push-credential-errors.ts +58 -0
- package/src/modules/communication_channels/lib/push-envelope.ts +104 -0
- package/src/modules/communication_channels/lib/refcounted-client-cache.ts +123 -0
- package/src/modules/communication_channels/migrations/.snapshot-open-mercato.json +9 -0
- package/src/modules/communication_channels/migrations/Migration20260703123630_communication_channels.ts +20 -0
- package/src/modules/communication_channels/notifications.ts +4 -0
- package/src/modules/communication_channels/setup.ts +2 -0
- package/src/modules/configs/i18n/de.json +5 -1
- package/src/modules/configs/i18n/en.json +5 -1
- package/src/modules/configs/i18n/es.json +5 -1
- package/src/modules/configs/i18n/ko.json +5 -1
- package/src/modules/configs/i18n/pl.json +5 -1
- package/src/modules/configs/lib/upgrade-actions.ts +21 -0
- package/src/modules/customer_accounts/i18n/de.json +2 -1
- package/src/modules/customer_accounts/i18n/en.json +2 -1
- package/src/modules/customer_accounts/i18n/es.json +2 -1
- package/src/modules/customer_accounts/i18n/ko.json +2 -1
- package/src/modules/customer_accounts/i18n/pl.json +2 -1
- package/src/modules/customer_accounts/notifications.ts +12 -0
- package/src/modules/customers/i18n/de.json +2 -1
- package/src/modules/customers/i18n/en.json +2 -1
- package/src/modules/customers/i18n/es.json +2 -1
- package/src/modules/customers/i18n/ko.json +2 -1
- package/src/modules/customers/i18n/pl.json +2 -1
- package/src/modules/customers/notifications.ts +4 -0
- package/src/modules/devices/AGENTS.md +101 -0
- package/src/modules/devices/acl.ts +7 -0
- package/src/modules/devices/api/[id]/route.ts +180 -0
- package/src/modules/devices/api/admin/devices/[id]/route.ts +235 -0
- package/src/modules/devices/api/admin/devices/route.ts +160 -0
- package/src/modules/devices/api/auth.ts +13 -0
- package/src/modules/devices/api/deviceList.ts +54 -0
- package/src/modules/devices/api/deviceOps.ts +186 -0
- package/src/modules/devices/api/openapi.ts +23 -0
- package/src/modules/devices/api/route.ts +165 -0
- package/src/modules/devices/backend/devices/[id]/page.meta.ts +12 -0
- package/src/modules/devices/backend/devices/[id]/page.tsx +164 -0
- package/src/modules/devices/backend/devices/create/page.meta.ts +12 -0
- package/src/modules/devices/backend/devices/create/page.tsx +81 -0
- package/src/modules/devices/backend/devices/page.meta.ts +21 -0
- package/src/modules/devices/backend/devices/page.tsx +259 -0
- package/src/modules/devices/commands/deactivate.ts +126 -0
- package/src/modules/devices/commands/index.ts +3 -0
- package/src/modules/devices/commands/register.ts +199 -0
- package/src/modules/devices/commands/shared.ts +119 -0
- package/src/modules/devices/commands/update.ts +142 -0
- package/src/modules/devices/data/entities.ts +79 -0
- package/src/modules/devices/data/validators.ts +65 -0
- package/src/modules/devices/di.ts +9 -0
- package/src/modules/devices/encryption.ts +15 -0
- package/src/modules/devices/events.ts +16 -0
- package/src/modules/devices/i18n/de.json +45 -0
- package/src/modules/devices/i18n/en.json +45 -0
- package/src/modules/devices/i18n/es.json +45 -0
- package/src/modules/devices/i18n/ko.json +45 -0
- package/src/modules/devices/i18n/pl.json +45 -0
- package/src/modules/devices/index.ts +15 -0
- package/src/modules/devices/lib/operationMetadata.ts +35 -0
- package/src/modules/devices/migrations/.snapshot-open-mercato.json +200 -0
- package/src/modules/devices/migrations/Migration20260602120000.ts +15 -0
- package/src/modules/devices/migrations/Migration20260630120000.ts +20 -0
- package/src/modules/devices/migrations/Migration20260701120000.ts +13 -0
- package/src/modules/devices/migrations/Migration20260721120000.ts +18 -0
- package/src/modules/devices/setup.ts +11 -0
- package/src/modules/entities/cli.ts +16 -2
- package/src/modules/entities/migrations/Migration20260722120000.ts +38 -0
- package/src/modules/inbox_ops/i18n/de.json +2 -1
- package/src/modules/inbox_ops/i18n/en.json +2 -1
- package/src/modules/inbox_ops/i18n/es.json +2 -1
- package/src/modules/inbox_ops/i18n/ko.json +2 -1
- package/src/modules/inbox_ops/i18n/pl.json +2 -1
- package/src/modules/inbox_ops/notifications.ts +2 -0
- package/src/modules/messages/i18n/de.json +2 -1
- package/src/modules/messages/i18n/en.json +2 -1
- package/src/modules/messages/i18n/es.json +2 -1
- package/src/modules/messages/i18n/ko.json +2 -1
- package/src/modules/messages/i18n/pl.json +2 -1
- package/src/modules/messages/notifications.ts +2 -0
- package/src/modules/notifications/AGENTS.md +54 -0
- package/src/modules/notifications/acl.ts +2 -0
- package/src/modules/notifications/api/admin/preferences/route.ts +193 -0
- package/src/modules/notifications/api/channels/route.ts +54 -0
- package/src/modules/notifications/api/preferences/route.ts +131 -0
- package/src/modules/notifications/api/route.ts +8 -1
- package/src/modules/notifications/api/types/[id]/channels/[channel]/route.ts +226 -0
- package/src/modules/notifications/api/types/route.ts +239 -0
- package/src/modules/notifications/api/unread-count/route.ts +5 -1
- package/src/modules/notifications/backend/notifications/user-preferences/page.meta.ts +26 -0
- package/src/modules/notifications/backend/notifications/user-preferences/page.tsx +12 -0
- package/src/modules/notifications/backend/profile/notification-preferences/page.meta.ts +31 -0
- package/src/modules/notifications/backend/profile/notification-preferences/page.tsx +12 -0
- package/src/modules/notifications/data/entities.ts +144 -0
- package/src/modules/notifications/data/validators.ts +116 -0
- package/src/modules/notifications/di.ts +7 -0
- package/src/modules/notifications/events.ts +23 -0
- package/src/modules/notifications/frontend/NotificationPreferenceMatrix.tsx +287 -0
- package/src/modules/notifications/frontend/NotificationPreferencesPageClient.tsx +155 -0
- package/src/modules/notifications/frontend/NotificationSettingsPageClient.tsx +273 -2
- package/src/modules/notifications/frontend/NotificationUserPreferencesAdminPageClient.tsx +214 -0
- package/src/modules/notifications/generators.ts +82 -0
- package/src/modules/notifications/i18n/de.json +43 -0
- package/src/modules/notifications/i18n/en.json +43 -0
- package/src/modules/notifications/i18n/es.json +43 -0
- package/src/modules/notifications/i18n/ko.json +43 -0
- package/src/modules/notifications/i18n/pl.json +44 -1
- package/src/modules/notifications/lib/delivery-strategies-registry.ts +30 -0
- package/src/modules/notifications/lib/deliveryStrategies.ts +40 -4
- package/src/modules/notifications/lib/derive-category.ts +15 -0
- package/src/modules/notifications/lib/notification-channel-registry.ts +59 -0
- package/src/modules/notifications/lib/notification-type-registry.ts +182 -0
- package/src/modules/notifications/lib/notificationCopy.ts +34 -0
- package/src/modules/notifications/lib/notificationFactory.ts +5 -1
- package/src/modules/notifications/lib/notificationMapper.ts +2 -0
- package/src/modules/notifications/lib/notificationPreferenceService.ts +138 -0
- package/src/modules/notifications/lib/notificationService.ts +129 -25
- package/src/modules/notifications/lib/notificationVisibility.ts +48 -0
- package/src/modules/notifications/lib/routeHelpers.ts +14 -1
- package/src/modules/notifications/lib/shouldDeliver.ts +112 -0
- package/src/modules/notifications/lib/strategies/email-delivery-strategy.ts +63 -0
- package/src/modules/notifications/lib/strategies/in-app-delivery-strategy.ts +21 -0
- package/src/modules/notifications/lib/typeCatalogue.ts +141 -0
- package/src/modules/notifications/lib/typeChannelSettings.ts +29 -0
- package/src/modules/notifications/lib/typeOverrides.ts +46 -0
- package/src/modules/notifications/migrations/.snapshot-open-mercato.json +874 -100
- package/src/modules/notifications/migrations/Migration20260625122947_notifications.ts +14 -0
- package/src/modules/notifications/migrations/Migration20260626120500_notifications_push_payload.ts +15 -0
- package/src/modules/notifications/migrations/Migration20260626130000_notifications_non_opt_out.ts +13 -0
- package/src/modules/notifications/migrations/Migration20260626140000_notifications_type_category_silent.ts +15 -0
- package/src/modules/notifications/migrations/Migration20260706120000_notifications_channels.ts +13 -0
- package/src/modules/notifications/migrations/Migration20260716123121_notifications.ts +14 -0
- package/src/modules/notifications/notification-channels.ts +30 -0
- package/src/modules/notifications/notifications.delivery-strategies.ts +15 -0
- package/src/modules/notifications/setup.ts +23 -1
- package/src/modules/notifications/subscribers/deliver-notification.ts +105 -71
- package/src/modules/notifications/subscribers/sync-notification-types.ts +18 -0
- package/src/modules/notifications/widgets/injection/profile-preferences-menu/widget.ts +35 -0
- package/src/modules/notifications/widgets/injection-table.ts +18 -0
- package/src/modules/push_notifications/AGENTS.md +92 -0
- package/src/modules/push_notifications/acl.ts +9 -0
- package/src/modules/push_notifications/api/custom-send/route.ts +175 -0
- package/src/modules/push_notifications/api/deliveries/[id]/route.ts +122 -0
- package/src/modules/push_notifications/api/deliveries/route.ts +58 -0
- package/src/modules/push_notifications/api/openapi.ts +22 -0
- package/src/modules/push_notifications/backend/push_notifications/[id]/page.meta.ts +9 -0
- package/src/modules/push_notifications/backend/push_notifications/[id]/page.tsx +149 -0
- package/src/modules/push_notifications/backend/push_notifications/page.meta.ts +21 -0
- package/src/modules/push_notifications/backend/push_notifications/page.tsx +239 -0
- package/src/modules/push_notifications/backend/push_notifications/send/page.meta.ts +24 -0
- package/src/modules/push_notifications/backend/push_notifications/send/page.tsx +327 -0
- package/src/modules/push_notifications/data/entities.ts +111 -0
- package/src/modules/push_notifications/data/extensions.ts +29 -0
- package/src/modules/push_notifications/data/validators.ts +102 -0
- package/src/modules/push_notifications/di.ts +20 -0
- package/src/modules/push_notifications/events.ts +21 -0
- package/src/modules/push_notifications/i18n/de.json +79 -0
- package/src/modules/push_notifications/i18n/en.json +79 -0
- package/src/modules/push_notifications/i18n/es.json +79 -0
- package/src/modules/push_notifications/i18n/ko.json +79 -0
- package/src/modules/push_notifications/i18n/pl.json +79 -0
- package/src/modules/push_notifications/index.ts +15 -0
- package/src/modules/push_notifications/lib/fake-provider-recorder.ts +154 -0
- package/src/modules/push_notifications/lib/push-delivery-strategy.ts +90 -0
- package/src/modules/push_notifications/lib/push-delivery.ts +455 -0
- package/src/modules/push_notifications/lib/push-fanout.ts +237 -0
- package/src/modules/push_notifications/lib/push-health.ts +47 -0
- package/src/modules/push_notifications/lib/push-reaper.ts +140 -0
- package/src/modules/push_notifications/lib/push-receipt-reaper.ts +210 -0
- package/src/modules/push_notifications/lib/push-stub-adapter.ts +80 -0
- package/src/modules/push_notifications/lib/queue.ts +74 -0
- package/src/modules/push_notifications/lib/reclaim-window.ts +16 -0
- package/src/modules/push_notifications/lib/send-custom-push.ts +100 -0
- package/src/modules/push_notifications/migrations/.snapshot-open-mercato.json +386 -0
- package/src/modules/push_notifications/migrations/Migration20260625150049_push_notifications.ts +16 -0
- package/src/modules/push_notifications/migrations/Migration20260626120000_push_notifications_silent.ts +13 -0
- package/src/modules/push_notifications/notifications.delivery-strategies.ts +8 -0
- package/src/modules/push_notifications/notifications.ts +50 -0
- package/src/modules/push_notifications/setup.ts +92 -0
- package/src/modules/push_notifications/workers/reclaim-stuck.worker.ts +78 -0
- package/src/modules/push_notifications/workers/send-push.worker.ts +34 -0
- package/src/modules/sales/i18n/de.json +1 -0
- package/src/modules/sales/i18n/en.json +1 -0
- package/src/modules/sales/i18n/es.json +1 -0
- package/src/modules/sales/i18n/ko.json +1 -0
- package/src/modules/sales/i18n/pl.json +1 -0
- package/src/modules/sales/notifications.ts +8 -0
- package/src/modules/staff/i18n/de.json +1 -0
- package/src/modules/staff/i18n/en.json +1 -0
- package/src/modules/staff/i18n/es.json +1 -0
- package/src/modules/staff/i18n/ko.json +1 -0
- package/src/modules/staff/i18n/pl.json +1 -0
- package/src/modules/staff/notifications.ts +6 -0
- package/src/modules/workflows/i18n/de.json +1 -0
- package/src/modules/workflows/i18n/en.json +1 -0
- package/src/modules/workflows/i18n/es.json +1 -0
- package/src/modules/workflows/i18n/ko.json +1 -0
- package/src/modules/workflows/i18n/pl.json +1 -0
- 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
|
+
}
|