@hed-hog/core 0.0.390 → 0.0.394
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/README.md +39 -0
- package/dist/access-log/access-log.controller.d.ts.map +1 -1
- package/dist/access-log/access-log.controller.js +1 -1
- package/dist/access-log/access-log.controller.js.map +1 -1
- package/dist/access-log/access-log.interceptor.d.ts.map +1 -1
- package/dist/access-log/access-log.interceptor.js +8 -5
- package/dist/access-log/access-log.interceptor.js.map +1 -1
- package/dist/access-log/access-log.service.d.ts +4 -0
- package/dist/access-log/access-log.service.d.ts.map +1 -1
- package/dist/access-log/access-log.service.js +31 -4
- package/dist/access-log/access-log.service.js.map +1 -1
- package/dist/access-log/dto/query-access-log.dto.d.ts +2 -0
- package/dist/access-log/dto/query-access-log.dto.d.ts.map +1 -1
- package/dist/access-log/dto/query-access-log.dto.js +5 -0
- package/dist/access-log/dto/query-access-log.dto.js.map +1 -1
- package/dist/ai/ai.controller.d.ts +12 -8
- package/dist/ai/ai.controller.d.ts.map +1 -1
- package/dist/ai/ai.controller.js +17 -2
- package/dist/ai/ai.controller.js.map +1 -1
- package/dist/ai/ai.module.d.ts.map +1 -1
- package/dist/ai/ai.module.js +3 -2
- package/dist/ai/ai.module.js.map +1 -1
- package/dist/ai/ai.service.d.ts +30 -1
- package/dist/ai/ai.service.d.ts.map +1 -1
- package/dist/ai/ai.service.js +97 -3
- package/dist/ai/ai.service.js.map +1 -1
- package/dist/ai/dto/richtext-edit-html.dto.d.ts +5 -0
- package/dist/ai/dto/richtext-edit-html.dto.d.ts.map +1 -0
- package/dist/ai/dto/richtext-edit-html.dto.js +27 -0
- package/dist/ai/dto/richtext-edit-html.dto.js.map +1 -0
- package/dist/ai/richtext-ai.prompts.d.ts +5 -0
- package/dist/ai/richtext-ai.prompts.d.ts.map +1 -0
- package/dist/ai/richtext-ai.prompts.js +57 -0
- package/dist/ai/richtext-ai.prompts.js.map +1 -0
- package/dist/ai/richtext-ai.service.d.ts +34 -0
- package/dist/ai/richtext-ai.service.d.ts.map +1 -0
- package/dist/ai/richtext-ai.service.js +95 -0
- package/dist/ai/richtext-ai.service.js.map +1 -0
- package/dist/ai-instruction/ai-instruction.controller.d.ts +5 -5
- package/dist/auth/auth.controller.d.ts +66 -7
- package/dist/auth/auth.controller.d.ts.map +1 -1
- package/dist/auth/auth.controller.js +67 -23
- package/dist/auth/auth.controller.js.map +1 -1
- package/dist/auth/auth.module.d.ts.map +1 -1
- package/dist/auth/auth.module.js +2 -0
- package/dist/auth/auth.module.js.map +1 -1
- package/dist/auth/auth.service.d.ts +136 -4
- package/dist/auth/auth.service.d.ts.map +1 -1
- package/dist/auth/auth.service.js +246 -48
- package/dist/auth/auth.service.js.map +1 -1
- package/dist/auth/dto/confirm-email.dto.d.ts +4 -0
- package/dist/auth/dto/confirm-email.dto.d.ts.map +1 -0
- package/dist/auth/dto/confirm-email.dto.js +22 -0
- package/dist/auth/dto/confirm-email.dto.js.map +1 -0
- package/dist/auth/dto/resend-confirmation.dto.d.ts +8 -0
- package/dist/auth/dto/resend-confirmation.dto.d.ts.map +1 -0
- package/dist/auth/dto/resend-confirmation.dto.js +36 -0
- package/dist/auth/dto/resend-confirmation.dto.js.map +1 -0
- package/dist/authorize/authorize.controller.d.ts +1 -1
- package/dist/authorize/authorize.controller.d.ts.map +1 -1
- package/dist/authorize/authorize.controller.js +4 -2
- package/dist/authorize/authorize.controller.js.map +1 -1
- package/dist/authorize/authorize.service.d.ts +7 -0
- package/dist/authorize/authorize.service.d.ts.map +1 -1
- package/dist/authorize/authorize.service.js +1 -1
- package/dist/authorize/authorize.service.js.map +1 -1
- package/dist/challenge/challenge.service.d.ts +18 -5
- package/dist/challenge/challenge.service.d.ts.map +1 -1
- package/dist/challenge/challenge.service.js +50 -3
- package/dist/challenge/challenge.service.js.map +1 -1
- package/dist/core.module.d.ts.map +1 -1
- package/dist/core.module.js +17 -0
- package/dist/core.module.js.map +1 -1
- package/dist/dashboard/dashboard/dashboard.controller.d.ts +6 -6
- package/dist/dashboard/dashboard/dashboard.service.d.ts +6 -6
- package/dist/dashboard/dashboard-component/dashboard-component.controller.d.ts +4 -4
- package/dist/dashboard/dashboard-component/dashboard-component.service.d.ts +4 -4
- package/dist/dashboard/dashboard-component-role/dashboard-component-role.controller.d.ts +1 -1
- package/dist/dashboard/dashboard-component-role/dashboard-component-role.service.d.ts +1 -1
- package/dist/dashboard/dashboard-core/dashboard-core.controller.d.ts +2 -2
- package/dist/dashboard/dashboard-core/dashboard-core.service.d.ts +2 -2
- package/dist/dashboard/dashboard-item/dashboard-item.controller.d.ts +2 -2
- package/dist/dashboard/dashboard-item/dashboard-item.service.d.ts +2 -2
- package/dist/file/file.controller.d.ts +8 -1
- package/dist/file/file.controller.d.ts.map +1 -1
- package/dist/file/file.controller.js +45 -7
- package/dist/file/file.controller.js.map +1 -1
- package/dist/file/file.service.d.ts +128 -1
- package/dist/file/file.service.d.ts.map +1 -1
- package/dist/file/file.service.js +319 -8
- package/dist/file/file.service.js.map +1 -1
- package/dist/file/image-optimization.service.d.ts +43 -0
- package/dist/file/image-optimization.service.d.ts.map +1 -1
- package/dist/file/image-optimization.service.js +154 -0
- package/dist/file/image-optimization.service.js.map +1 -1
- package/dist/file/provider/abstract.provider.d.ts +16 -0
- package/dist/file/provider/abstract.provider.d.ts.map +1 -1
- package/dist/file/provider/abstract.provider.js +4 -0
- package/dist/file/provider/abstract.provider.js.map +1 -1
- package/dist/file/provider/gcs.provider.d.ts +2 -0
- package/dist/file/provider/gcs.provider.d.ts.map +1 -1
- package/dist/file/provider/gcs.provider.js +10 -0
- package/dist/file/provider/gcs.provider.js.map +1 -1
- package/dist/file/provider/local.provider.d.ts +1 -0
- package/dist/file/provider/local.provider.d.ts.map +1 -1
- package/dist/file/provider/local.provider.js +13 -0
- package/dist/file/provider/local.provider.js.map +1 -1
- package/dist/file/provider/s3.provider.d.ts +5 -0
- package/dist/file/provider/s3.provider.d.ts.map +1 -1
- package/dist/file/provider/s3.provider.js +15 -0
- package/dist/file/provider/s3.provider.js.map +1 -1
- package/dist/file/provider/spaces.provider.d.ts +5 -0
- package/dist/file/provider/spaces.provider.d.ts.map +1 -1
- package/dist/file/provider/spaces.provider.js +16 -0
- package/dist/file/provider/spaces.provider.js.map +1 -1
- package/dist/impersonation/dto/redeem-impersonation.dto.d.ts +4 -0
- package/dist/impersonation/dto/redeem-impersonation.dto.d.ts.map +1 -0
- package/dist/impersonation/dto/redeem-impersonation.dto.js +23 -0
- package/dist/impersonation/dto/redeem-impersonation.dto.js.map +1 -0
- package/dist/impersonation/dto/start-impersonation.dto.d.ts +7 -0
- package/dist/impersonation/dto/start-impersonation.dto.d.ts.map +1 -0
- package/dist/impersonation/dto/start-impersonation.dto.js +27 -0
- package/dist/impersonation/dto/start-impersonation.dto.js.map +1 -0
- package/dist/impersonation/impersonation-restrictions.d.ts +26 -0
- package/dist/impersonation/impersonation-restrictions.d.ts.map +1 -0
- package/dist/impersonation/impersonation-restrictions.js +76 -0
- package/dist/impersonation/impersonation-restrictions.js.map +1 -0
- package/dist/impersonation/impersonation.controller.d.ts +90 -0
- package/dist/impersonation/impersonation.controller.d.ts.map +1 -0
- package/dist/impersonation/impersonation.controller.js +135 -0
- package/dist/impersonation/impersonation.controller.js.map +1 -0
- package/dist/impersonation/impersonation.module.d.ts +8 -0
- package/dist/impersonation/impersonation.module.d.ts.map +1 -0
- package/dist/impersonation/impersonation.module.js +44 -0
- package/dist/impersonation/impersonation.module.js.map +1 -0
- package/dist/impersonation/impersonation.service.d.ts +164 -0
- package/dist/impersonation/impersonation.service.d.ts.map +1 -0
- package/dist/impersonation/impersonation.service.js +408 -0
- package/dist/impersonation/impersonation.service.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -1
- package/dist/integration/services/outbox-polling.coordinator.d.ts +2 -0
- package/dist/integration/services/outbox-polling.coordinator.d.ts.map +1 -1
- package/dist/integration/services/outbox-polling.coordinator.js +50 -30
- package/dist/integration/services/outbox-polling.coordinator.js.map +1 -1
- package/dist/integration/services/outbox.service.d.ts.map +1 -1
- package/dist/integration/services/outbox.service.js +5 -1
- package/dist/integration/services/outbox.service.js.map +1 -1
- package/dist/integration-profile/integration-profile.secrets.d.ts.map +1 -1
- package/dist/integration-profile/integration-profile.secrets.js +3 -0
- package/dist/integration-profile/integration-profile.secrets.js.map +1 -1
- package/dist/integration-profile/integration-profile.utils.d.ts +14 -0
- package/dist/integration-profile/integration-profile.utils.d.ts.map +1 -1
- package/dist/integration-profile/integration-profile.utils.js +26 -0
- package/dist/integration-profile/integration-profile.utils.js.map +1 -1
- package/dist/integration-profile/testers/email.testers.d.ts.map +1 -1
- package/dist/integration-profile/testers/email.testers.js +28 -0
- package/dist/integration-profile/testers/email.testers.js.map +1 -1
- package/dist/language/en.json +11 -0
- package/dist/language/pt.json +11 -0
- package/dist/mail/dto/send.dto.d.ts +38 -1
- package/dist/mail/dto/send.dto.d.ts.map +1 -1
- package/dist/mail/dto/send.dto.js +10 -0
- package/dist/mail/dto/send.dto.js.map +1 -1
- package/dist/mail/mail.controller.d.ts +1 -0
- package/dist/mail/mail.controller.d.ts.map +1 -1
- package/dist/mail/mail.module.d.ts.map +1 -1
- package/dist/mail/mail.module.js +2 -0
- package/dist/mail/mail.module.js.map +1 -1
- package/dist/mail/mail.service.d.ts +48 -4
- package/dist/mail/mail.service.d.ts.map +1 -1
- package/dist/mail/mail.service.js +187 -35
- package/dist/mail/mail.service.js.map +1 -1
- package/dist/mail-delivery/mail-delivery-provision.service.d.ts +21 -0
- package/dist/mail-delivery/mail-delivery-provision.service.d.ts.map +1 -0
- package/dist/mail-delivery/mail-delivery-provision.service.js +64 -0
- package/dist/mail-delivery/mail-delivery-provision.service.js.map +1 -0
- package/dist/mail-delivery/mail-delivery.constants.d.ts +17 -0
- package/dist/mail-delivery/mail-delivery.constants.d.ts.map +1 -0
- package/dist/mail-delivery/mail-delivery.constants.js +31 -0
- package/dist/mail-delivery/mail-delivery.constants.js.map +1 -0
- package/dist/mail-delivery/mail-delivery.controller.d.ts +19 -0
- package/dist/mail-delivery/mail-delivery.controller.d.ts.map +1 -0
- package/dist/mail-delivery/mail-delivery.controller.js +53 -0
- package/dist/mail-delivery/mail-delivery.controller.js.map +1 -0
- package/dist/mail-delivery/mail-delivery.module.d.ts +7 -0
- package/dist/mail-delivery/mail-delivery.module.d.ts.map +1 -0
- package/dist/mail-delivery/mail-delivery.module.js +36 -0
- package/dist/mail-delivery/mail-delivery.module.js.map +1 -0
- package/dist/mail-delivery/mail-delivery.service.d.ts +38 -0
- package/dist/mail-delivery/mail-delivery.service.d.ts.map +1 -0
- package/dist/mail-delivery/mail-delivery.service.js +140 -0
- package/dist/mail-delivery/mail-delivery.service.js.map +1 -0
- package/dist/mail-delivery/ses-event.parser.d.ts +27 -0
- package/dist/mail-delivery/ses-event.parser.d.ts.map +1 -0
- package/dist/mail-delivery/ses-event.parser.js +131 -0
- package/dist/mail-delivery/ses-event.parser.js.map +1 -0
- package/dist/mail-delivery/ses-webhook.service.d.ts +58 -0
- package/dist/mail-delivery/ses-webhook.service.d.ts.map +1 -0
- package/dist/mail-delivery/ses-webhook.service.js +214 -0
- package/dist/mail-delivery/ses-webhook.service.js.map +1 -0
- package/dist/mail-delivery/sns-message.util.d.ts +38 -0
- package/dist/mail-delivery/sns-message.util.d.ts.map +1 -0
- package/dist/mail-delivery/sns-message.util.js +109 -0
- package/dist/mail-delivery/sns-message.util.js.map +1 -0
- package/dist/mail-sent/dto/query-mail-sent-list.dto.d.ts +5 -0
- package/dist/mail-sent/dto/query-mail-sent-list.dto.d.ts.map +1 -1
- package/dist/mail-sent/dto/query-mail-sent-list.dto.js +6 -0
- package/dist/mail-sent/dto/query-mail-sent-list.dto.js.map +1 -1
- package/dist/mail-sent/mail-sent.controller.d.ts +39 -0
- package/dist/mail-sent/mail-sent.controller.d.ts.map +1 -1
- package/dist/mail-sent/mail-sent.service.d.ts +39 -0
- package/dist/mail-sent/mail-sent.service.d.ts.map +1 -1
- package/dist/mail-sent/mail-sent.service.js +45 -4
- package/dist/mail-sent/mail-sent.service.js.map +1 -1
- package/dist/mailbox/mailbox-fetcher.registry.d.ts +100 -0
- package/dist/mailbox/mailbox-fetcher.registry.d.ts.map +1 -0
- package/dist/mailbox/mailbox-fetcher.registry.js +59 -0
- package/dist/mailbox/mailbox-fetcher.registry.js.map +1 -0
- package/dist/mailbox/mailbox.module.d.ts +8 -0
- package/dist/mailbox/mailbox.module.d.ts.map +1 -0
- package/dist/mailbox/mailbox.module.js +27 -0
- package/dist/mailbox/mailbox.module.js.map +1 -0
- package/dist/meeting/meeting-provider.registry.d.ts +155 -0
- package/dist/meeting/meeting-provider.registry.d.ts.map +1 -0
- package/dist/meeting/meeting-provider.registry.js +53 -0
- package/dist/meeting/meeting-provider.registry.js.map +1 -0
- package/dist/meeting/meeting.constants.d.ts +47 -0
- package/dist/meeting/meeting.constants.d.ts.map +1 -0
- package/dist/meeting/meeting.constants.js +21 -0
- package/dist/meeting/meeting.constants.js.map +1 -0
- package/dist/meeting/meeting.module.d.ts +7 -0
- package/dist/meeting/meeting.module.d.ts.map +1 -0
- package/dist/meeting/meeting.module.js +26 -0
- package/dist/meeting/meeting.module.js.map +1 -0
- package/dist/middlewares/locale-injection.middleware.d.ts +12 -0
- package/dist/middlewares/locale-injection.middleware.d.ts.map +1 -1
- package/dist/middlewares/locale-injection.middleware.js +20 -4
- package/dist/middlewares/locale-injection.middleware.js.map +1 -1
- package/dist/oauth/providers/google.provider.d.ts +7 -0
- package/dist/oauth/providers/google.provider.d.ts.map +1 -1
- package/dist/oauth/providers/google.provider.js +43 -13
- package/dist/oauth/providers/google.provider.js.map +1 -1
- package/dist/observability/monitored-cron.d.ts +34 -0
- package/dist/observability/monitored-cron.d.ts.map +1 -0
- package/dist/observability/monitored-cron.js +118 -0
- package/dist/observability/monitored-cron.js.map +1 -0
- package/dist/pairing/dto/approve-reverse-pairing.dto.d.ts +12 -0
- package/dist/pairing/dto/approve-reverse-pairing.dto.d.ts.map +1 -0
- package/dist/pairing/dto/approve-reverse-pairing.dto.js +30 -0
- package/dist/pairing/dto/approve-reverse-pairing.dto.js.map +1 -0
- package/dist/pairing/dto/claim-pairing.dto.d.ts +11 -0
- package/dist/pairing/dto/claim-pairing.dto.d.ts.map +1 -0
- package/dist/pairing/dto/claim-pairing.dto.js +35 -0
- package/dist/pairing/dto/claim-pairing.dto.js.map +1 -0
- package/dist/pairing/dto/confirm-pairing.dto.d.ts +10 -0
- package/dist/pairing/dto/confirm-pairing.dto.d.ts.map +1 -0
- package/dist/pairing/dto/confirm-pairing.dto.js +24 -0
- package/dist/pairing/dto/confirm-pairing.dto.js.map +1 -0
- package/dist/pairing/dto/poll-reverse-pairing.dto.d.ts +5 -0
- package/dist/pairing/dto/poll-reverse-pairing.dto.d.ts.map +1 -0
- package/dist/pairing/dto/poll-reverse-pairing.dto.js +24 -0
- package/dist/pairing/dto/poll-reverse-pairing.dto.js.map +1 -0
- package/dist/pairing/dto/start-pairing.dto.d.ts +11 -0
- package/dist/pairing/dto/start-pairing.dto.d.ts.map +1 -0
- package/dist/pairing/dto/start-pairing.dto.js +30 -0
- package/dist/pairing/dto/start-pairing.dto.js.map +1 -0
- package/dist/pairing/dto/start-reverse-pairing.dto.d.ts +24 -0
- package/dist/pairing/dto/start-reverse-pairing.dto.d.ts.map +1 -0
- package/dist/pairing/dto/start-reverse-pairing.dto.js +47 -0
- package/dist/pairing/dto/start-reverse-pairing.dto.js.map +1 -0
- package/dist/pairing/pairing.controller.d.ts +118 -0
- package/dist/pairing/pairing.controller.d.ts.map +1 -0
- package/dist/pairing/pairing.controller.js +296 -0
- package/dist/pairing/pairing.controller.js.map +1 -0
- package/dist/pairing/pairing.module.d.ts +10 -0
- package/dist/pairing/pairing.module.d.ts.map +1 -0
- package/dist/pairing/pairing.module.js +40 -0
- package/dist/pairing/pairing.module.js.map +1 -0
- package/dist/pairing/pairing.service.d.ts +238 -0
- package/dist/pairing/pairing.service.d.ts.map +1 -0
- package/dist/pairing/pairing.service.js +495 -0
- package/dist/pairing/pairing.service.js.map +1 -0
- package/dist/pdf/pdf.service.d.ts +3 -6
- package/dist/pdf/pdf.service.d.ts.map +1 -1
- package/dist/pdf/pdf.service.js +8 -20
- package/dist/pdf/pdf.service.js.map +1 -1
- package/dist/profile/profile.controller.d.ts +11 -0
- package/dist/profile/profile.controller.d.ts.map +1 -1
- package/dist/profile/profile.service.d.ts +11 -0
- package/dist/profile/profile.service.d.ts.map +1 -1
- package/dist/profile/profile.service.js +20 -11
- package/dist/profile/profile.service.js.map +1 -1
- package/dist/redis/redis-pubsub.d.ts.map +1 -1
- package/dist/redis/redis-pubsub.js +44 -0
- package/dist/redis/redis-pubsub.js.map +1 -1
- package/dist/role/guards/role.guard.d.ts +5 -0
- package/dist/role/guards/role.guard.d.ts.map +1 -1
- package/dist/role/guards/role.guard.js +41 -39
- package/dist/role/guards/role.guard.js.map +1 -1
- package/dist/session/session.controller.d.ts +2 -0
- package/dist/session/session.controller.d.ts.map +1 -1
- package/dist/session/session.service.d.ts +39 -0
- package/dist/session/session.service.d.ts.map +1 -1
- package/dist/session/session.service.js +91 -8
- package/dist/session/session.service.js.map +1 -1
- package/dist/setting/brand-asset.service.d.ts +48 -0
- package/dist/setting/brand-asset.service.d.ts.map +1 -0
- package/dist/setting/brand-asset.service.js +130 -0
- package/dist/setting/brand-asset.service.js.map +1 -0
- package/dist/setting/setting.module.d.ts.map +1 -1
- package/dist/setting/setting.module.js +3 -2
- package/dist/setting/setting.module.js.map +1 -1
- package/dist/setting/setting.service.d.ts +33 -0
- package/dist/setting/setting.service.d.ts.map +1 -1
- package/dist/setting/setting.service.js +88 -0
- package/dist/setting/setting.service.js.map +1 -1
- package/dist/token/token.service.d.ts +23 -2
- package/dist/token/token.service.d.ts.map +1 -1
- package/dist/token/token.service.js +39 -4
- package/dist/token/token.service.js.map +1 -1
- package/dist/user/user.controller.d.ts +8 -22
- package/dist/user/user.controller.d.ts.map +1 -1
- package/dist/user/user.service.d.ts +44 -24
- package/dist/user/user.service.d.ts.map +1 -1
- package/dist/user/user.service.js +89 -6
- package/dist/user/user.service.js.map +1 -1
- package/dist/user-device/user-device.controller.d.ts +54 -0
- package/dist/user-device/user-device.controller.d.ts.map +1 -0
- package/dist/user-device/user-device.controller.js +103 -0
- package/dist/user-device/user-device.controller.js.map +1 -0
- package/dist/user-device/user-device.module.d.ts +3 -0
- package/dist/user-device/user-device.module.d.ts.map +1 -0
- package/dist/user-device/user-device.module.js +26 -0
- package/dist/user-device/user-device.module.js.map +1 -0
- package/dist/user-device/user-device.service.d.ts +71 -0
- package/dist/user-device/user-device.service.d.ts.map +1 -0
- package/dist/user-device/user-device.service.js +158 -0
- package/dist/user-device/user-device.service.js.map +1 -0
- package/dist/utils/device-key.d.ts +23 -0
- package/dist/utils/device-key.d.ts.map +1 -0
- package/dist/utils/device-key.js +31 -0
- package/dist/utils/device-key.js.map +1 -0
- package/dist/utils/geo-ip.d.ts +38 -0
- package/dist/utils/geo-ip.d.ts.map +1 -0
- package/dist/utils/geo-ip.js +122 -0
- package/dist/utils/geo-ip.js.map +1 -0
- package/dist/utils/locale-context.d.ts +10 -7
- package/dist/utils/locale-context.d.ts.map +1 -1
- package/dist/utils/locale-context.js +14 -8
- package/dist/utils/locale-context.js.map +1 -1
- package/dist/utils/security-event-vars.d.ts +56 -0
- package/dist/utils/security-event-vars.d.ts.map +1 -0
- package/dist/utils/security-event-vars.js +109 -0
- package/dist/utils/security-event-vars.js.map +1 -0
- package/dist/utils/user-agent.d.ts +19 -0
- package/dist/utils/user-agent.d.ts.map +1 -0
- package/dist/utils/user-agent.js +36 -0
- package/dist/utils/user-agent.js.map +1 -0
- package/dist/webhook-integration/webhook-integration.service.d.ts +5 -0
- package/dist/webhook-integration/webhook-integration.service.d.ts.map +1 -1
- package/dist/webhook-integration/webhook-integration.service.js +50 -4
- package/dist/webhook-integration/webhook-integration.service.js.map +1 -1
- package/dist/webhook-integration/webhook-protocol.registry.d.ts +6 -0
- package/dist/webhook-integration/webhook-protocol.registry.d.ts.map +1 -1
- package/dist/webhook-integration/webhook-protocol.registry.js.map +1 -1
- package/hedhog/data/integration_provider.yaml +9 -0
- package/hedhog/data/mail.yaml +198 -62
- package/hedhog/data/mail_layout.yaml +43 -33
- package/hedhog/data/menu.yaml +2 -0
- package/hedhog/data/role.yaml +16 -0
- package/hedhog/data/route.yaml +172 -3
- package/hedhog/data/setting_group.yaml +178 -2
- package/hedhog/data/setting_subgroup.yaml +8 -0
- package/hedhog/frontend/app/_lib/hooks/use-core-access.test.tsx.ejs +125 -0
- package/hedhog/frontend/app/_lib/hooks/use-core-access.ts.ejs +29 -0
- package/hedhog/frontend/app/_lib/hooks/use-impersonation-access.test.ts.ejs +71 -0
- package/hedhog/frontend/app/_lib/hooks/use-impersonation-access.ts.ejs +32 -0
- package/hedhog/frontend/app/access-log/page.tsx.ejs +96 -22
- package/hedhog/frontend/app/account/components/active-sessions.tsx.ejs +17 -1
- package/hedhog/frontend/app/account/components/known-devices.tsx.ejs +89 -0
- package/hedhog/frontend/app/account/components/profile-form.tsx.ejs +2 -1
- package/hedhog/frontend/app/account/components/user-devices-list.tsx.ejs +186 -0
- package/hedhog/frontend/app/account/devices/page.tsx.ejs +5 -0
- package/hedhog/frontend/app/account/layout.tsx.ejs +2 -0
- package/hedhog/frontend/app/configurations/[slug]/components/setting-field.test.tsx.ejs +298 -0
- package/hedhog/frontend/app/configurations/[slug]/components/setting-field.tsx.ejs +376 -1236
- package/hedhog/frontend/app/mail/log/page.tsx.ejs +210 -3
- package/hedhog/frontend/app/mail/template/page.tsx.ejs +190 -132
- package/hedhog/frontend/app/users/active-session.tsx.ejs +58 -11
- package/hedhog/frontend/app/users/page.tsx.ejs +69 -10
- package/hedhog/frontend/app/users/simulate-access-dialog.test.tsx.ejs +202 -0
- package/hedhog/frontend/app/users/simulate-access-dialog.tsx.ejs +170 -0
- package/hedhog/frontend/app/users/user-activity-timeline.tsx.ejs +19 -0
- package/hedhog/frontend/app/users/user-devices.tsx.ejs +89 -0
- package/hedhog/frontend/app/users/user-logs.tsx.ejs +13 -1
- package/hedhog/frontend/app/users/user-sheet.tsx.ejs +46 -24
- package/hedhog/frontend/messages/en.json +146 -48
- package/hedhog/frontend/messages/pt.json +146 -48
- package/hedhog/table/access_log.yaml +7 -0
- package/hedhog/table/file.yaml +8 -0
- package/hedhog/table/mail_delivery_event.yaml +76 -0
- package/hedhog/table/mail_sent.yaml +23 -0
- package/hedhog/table/user_activity.yaml +9 -0
- package/hedhog/table/user_device.yaml +52 -0
- package/hedhog/table/user_session.yaml +20 -1
- package/package.json +6 -5
- package/src/access-log/access-log.controller.ts +1 -0
- package/src/access-log/access-log.interceptor.ts +5 -1
- package/src/access-log/access-log.service.spec.ts +33 -2
- package/src/access-log/access-log.service.ts +38 -0
- package/src/access-log/dto/query-access-log.dto.ts +5 -0
- package/src/ai/ai.controller.ts +9 -0
- package/src/ai/ai.module.ts +3 -2
- package/src/ai/ai.service.ts +150 -13
- package/src/ai/dto/richtext-edit-html.dto.ts +11 -0
- package/src/ai/richtext-ai.prompts.ts +57 -0
- package/src/ai/richtext-ai.service.ts +109 -0
- package/src/auth/auth.controller.ts +47 -8
- package/src/auth/auth.module.ts +2 -0
- package/src/auth/auth.service.spec.ts +383 -6
- package/src/auth/auth.service.ts +345 -56
- package/src/auth/dto/confirm-email.dto.ts +7 -0
- package/src/auth/dto/resend-confirmation.dto.ts +20 -0
- package/src/authorize/authorize.controller.ts +2 -0
- package/src/authorize/authorize.service.spec.ts +23 -0
- package/src/authorize/authorize.service.ts +8 -0
- package/src/challenge/challenge.service.spec.ts +73 -0
- package/src/challenge/challenge.service.ts +62 -3
- package/src/core.module.ts +19 -0
- package/src/file/file.controller.ts +54 -4
- package/src/file/file.service.spec.ts +275 -105
- package/src/file/file.service.ts +409 -14
- package/src/file/image-optimization.service.spec.ts +92 -0
- package/src/file/image-optimization.service.ts +202 -0
- package/src/file/provider/abstract.provider.ts +23 -0
- package/src/file/provider/gcs.provider.ts +17 -0
- package/src/file/provider/local.provider.ts +21 -0
- package/src/file/provider/s3.provider.ts +25 -0
- package/src/file/provider/spaces.provider.ts +26 -0
- package/src/impersonation/dto/redeem-impersonation.dto.ts +8 -0
- package/src/impersonation/dto/start-impersonation.dto.ts +13 -0
- package/src/impersonation/impersonation-restrictions.ts +84 -0
- package/src/impersonation/impersonation.controller.ts +109 -0
- package/src/impersonation/impersonation.module.ts +31 -0
- package/src/impersonation/impersonation.service.spec.ts +427 -0
- package/src/impersonation/impersonation.service.ts +577 -0
- package/src/index.ts +21 -0
- package/src/integration/services/outbox-polling.coordinator.spec.ts +90 -0
- package/src/integration/services/outbox-polling.coordinator.ts +62 -40
- package/src/integration/services/outbox.service.spec.ts +8 -0
- package/src/integration/services/outbox.service.ts +19 -13
- package/src/integration-profile/integration-profile.secrets.ts +3 -0
- package/src/integration-profile/integration-profile.utils.ts +38 -0
- package/src/integration-profile/testers/email.testers.ts +34 -1
- package/src/language/en.json +11 -0
- package/src/language/pt.json +11 -0
- package/src/mail/dto/send.dto.ts +46 -2
- package/src/mail/mail-template.spec.ts +79 -2
- package/src/mail/mail.module.ts +2 -0
- package/src/mail/mail.service.spec.ts +307 -6
- package/src/mail/mail.service.ts +227 -30
- package/src/mail-delivery/mail-delivery-provision.service.ts +66 -0
- package/src/mail-delivery/mail-delivery.constants.ts +31 -0
- package/src/mail-delivery/mail-delivery.controller.ts +38 -0
- package/src/mail-delivery/mail-delivery.module.ts +23 -0
- package/src/mail-delivery/mail-delivery.service.spec.ts +136 -0
- package/src/mail-delivery/mail-delivery.service.ts +152 -0
- package/src/mail-delivery/ses-event.parser.spec.ts +140 -0
- package/src/mail-delivery/ses-event.parser.ts +191 -0
- package/src/mail-delivery/ses-webhook.service.spec.ts +170 -0
- package/src/mail-delivery/ses-webhook.service.ts +240 -0
- package/src/mail-delivery/sns-message.util.spec.ts +163 -0
- package/src/mail-delivery/sns-message.util.ts +125 -0
- package/src/mail-sent/dto/query-mail-sent-list.dto.ts +15 -0
- package/src/mail-sent/mail-sent.service.ts +46 -2
- package/src/mailbox/mailbox-fetcher.registry.ts +132 -0
- package/src/mailbox/mailbox.module.ts +14 -0
- package/src/meeting/meeting-provider.registry.ts +193 -0
- package/src/meeting/meeting.constants.ts +53 -0
- package/src/meeting/meeting.module.ts +13 -0
- package/src/middlewares/locale-injection.middleware.ts +25 -6
- package/src/oauth/providers/google.provider.ts +49 -15
- package/src/observability/monitored-cron.spec.ts +102 -0
- package/src/observability/monitored-cron.ts +110 -0
- package/src/pairing/dto/approve-reverse-pairing.dto.ts +20 -0
- package/src/pairing/dto/claim-pairing.dto.ts +19 -0
- package/src/pairing/dto/confirm-pairing.dto.ts +14 -0
- package/src/pairing/dto/poll-reverse-pairing.dto.ts +9 -0
- package/src/pairing/dto/start-pairing.dto.ts +19 -0
- package/src/pairing/dto/start-reverse-pairing.dto.ts +45 -0
- package/src/pairing/pairing.controller.ts +246 -0
- package/src/pairing/pairing.module.ts +27 -0
- package/src/pairing/pairing.service.spec.ts +462 -0
- package/src/pairing/pairing.service.ts +614 -0
- package/src/pdf/pdf.service.ts +6 -24
- package/src/profile/profile.service.ts +26 -8
- package/src/redis/redis-pubsub.ts +11 -0
- package/src/role/guards/role.guard.spec.ts +75 -0
- package/src/role/guards/role.guard.ts +64 -46
- package/src/session/session.service.spec.ts +81 -0
- package/src/session/session.service.ts +93 -6
- package/src/setting/brand-asset.service.ts +137 -0
- package/src/setting/setting.module.ts +3 -2
- package/src/setting/setting.service.spec.ts +79 -1
- package/src/setting/setting.service.ts +109 -1
- package/src/token/token.service.spec.ts +62 -0
- package/src/token/token.service.ts +57 -3
- package/src/user/user.service.spec.ts +33 -2
- package/src/user/user.service.ts +108 -6
- package/src/user-device/user-device.controller.ts +77 -0
- package/src/user-device/user-device.module.ts +13 -0
- package/src/user-device/user-device.service.spec.ts +234 -0
- package/src/user-device/user-device.service.ts +168 -0
- package/src/utils/device-key.spec.ts +57 -0
- package/src/utils/device-key.ts +32 -0
- package/src/utils/geo-ip.spec.ts +134 -0
- package/src/utils/geo-ip.ts +93 -0
- package/src/utils/locale-context.spec.ts +27 -1
- package/src/utils/locale-context.ts +15 -12
- package/src/utils/security-event-vars.spec.ts +161 -0
- package/src/utils/security-event-vars.ts +153 -0
- package/src/utils/user-agent.spec.ts +37 -0
- package/src/utils/user-agent.ts +46 -0
- package/src/webhook-integration/webhook-integration.service.ts +57 -3
- package/src/webhook-integration/webhook-protocol.registry.ts +6 -0
|
@@ -24,6 +24,7 @@ import { SecurityService } from '../security/security.service';
|
|
|
24
24
|
import { SettingService } from '../setting/setting.service';
|
|
25
25
|
import { TokenService } from '../token/token.service';
|
|
26
26
|
import { UserService } from '../user/user.service';
|
|
27
|
+
import { buildSecurityEventVars } from '../utils/security-event-vars';
|
|
27
28
|
import { resolveWebAuthnOrigin } from '../utils/webauthn-origin';
|
|
28
29
|
import { ChangeEmailDto } from './dto/change-email.dto';
|
|
29
30
|
import { ChangePasswordDto } from './dto/change-password.dto';
|
|
@@ -322,9 +323,18 @@ export class ProfileService {
|
|
|
322
323
|
}
|
|
323
324
|
|
|
324
325
|
async getProfile(userId: number) {
|
|
325
|
-
|
|
326
|
-
where: { id: userId },
|
|
327
|
-
|
|
326
|
+
const [user, passwordCredentials] = await Promise.all([
|
|
327
|
+
this.prisma.user.findUnique({ where: { id: userId } }),
|
|
328
|
+
this.prisma.user_credential.count({ where: { user_id: userId, type: 'password' } }),
|
|
329
|
+
]);
|
|
330
|
+
|
|
331
|
+
if (!user) return user;
|
|
332
|
+
|
|
333
|
+
// Quem entrou só por login social não tem senha para digitar. Sem este
|
|
334
|
+
// campo o cliente não tem como saber disso e acaba exigindo a senha em
|
|
335
|
+
// telas onde `deleteAccount`/`changePassword` já a dispensam — o que deixa
|
|
336
|
+
// essas contas sem caminho para excluir a própria conta.
|
|
337
|
+
return { ...user, hasPassword: passwordCredentials > 0 };
|
|
328
338
|
}
|
|
329
339
|
|
|
330
340
|
async getMFAMethods(userId: number) {
|
|
@@ -477,14 +487,22 @@ export class ProfileService {
|
|
|
477
487
|
});
|
|
478
488
|
|
|
479
489
|
if (user?.user_identifier?.[0]?.value) {
|
|
480
|
-
|
|
481
|
-
|
|
490
|
+
const recipientEmail = user.user_identifier[0].value;
|
|
491
|
+
// Data e pais sao formatados aqui, entao o idioma tem que ser o mesmo que
|
|
492
|
+
// o corpo do e-mail vai usar - resolvido antes, nao dentro do envio.
|
|
493
|
+
const recipientLocale = await this.mail.resolveRecipientLocale(
|
|
494
|
+
recipientEmail,
|
|
495
|
+
locale,
|
|
496
|
+
userId,
|
|
497
|
+
);
|
|
498
|
+
|
|
499
|
+
this.mail.sendTemplatedMail(recipientLocale, {
|
|
500
|
+
email: recipientEmail,
|
|
482
501
|
slug: 'auth-change-password-notification',
|
|
502
|
+
userId,
|
|
483
503
|
variables: {
|
|
484
504
|
name: user.name,
|
|
485
|
-
userAgent:
|
|
486
|
-
ipAddress: ipAddress || 'Unknown',
|
|
487
|
-
location: 'Unknown',
|
|
505
|
+
...buildSecurityEventVars({ ipAddress, userAgent, locale: recipientLocale }),
|
|
488
506
|
},
|
|
489
507
|
app: await this.challenge.resolveAppKeyFromOrigin(origin),
|
|
490
508
|
}).catch(error => {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as Sentry from '@sentry/nestjs';
|
|
1
2
|
import { Logger } from '@nestjs/common';
|
|
2
3
|
import { createClient, type RedisClientType } from 'redis';
|
|
3
4
|
|
|
@@ -132,6 +133,16 @@ export async function createRedisPubSub(
|
|
|
132
133
|
try {
|
|
133
134
|
await onMessage(message);
|
|
134
135
|
} catch (error) {
|
|
136
|
+
// A instrumentação automática do Sentry para node-redis cobre publish()
|
|
137
|
+
// mas NÃO subscribe()/entrega de mensagem (limitação da instrumentação
|
|
138
|
+
// vendorizada, não deste projeto) — sem esta captura explícita, uma
|
|
139
|
+
// falha aqui era só um logger.warn, invisível fora do log do pod.
|
|
140
|
+
Sentry.withScope((scope) => {
|
|
141
|
+
scope.setTags({ 'redis.channel': channel, 'redis.label': label });
|
|
142
|
+
Sentry.captureException(error, {
|
|
143
|
+
mechanism: { handled: true, type: 'hedhog.redis_pubsub' },
|
|
144
|
+
});
|
|
145
|
+
});
|
|
135
146
|
logger.warn(
|
|
136
147
|
`Failed to handle Redis ${label} message: ${extractRedisErrorMessage(error)}`,
|
|
137
148
|
);
|
|
@@ -154,4 +154,79 @@ describe('RoleGuard', () => {
|
|
|
154
154
|
true,
|
|
155
155
|
);
|
|
156
156
|
});
|
|
157
|
+
|
|
158
|
+
describe('simulação de acesso — denylist de rotas', () => {
|
|
159
|
+
const impersonatedRequest = {
|
|
160
|
+
headers: { authorization: 'Bearer tok' },
|
|
161
|
+
auth: { sub: 42, sessionId: 555, act: 7, imp: true },
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
it('bloqueia GET /auth/desktop-token, que é @NoRole()', async () => {
|
|
165
|
+
// Sem esta guarda o operador cunharia para o ALVO uma sessão completa,
|
|
166
|
+
// renovável e de 30 dias — furando o teto da simulação.
|
|
167
|
+
const { guard, deps } = makeService();
|
|
168
|
+
setMeta(deps.reflector, { withNoRole: true });
|
|
169
|
+
setPath(deps.reflector, 'auth', 'desktop-token', RequestMethod.GET);
|
|
170
|
+
|
|
171
|
+
await expect(
|
|
172
|
+
guard.canActivate(makeContext(impersonatedRequest) as any),
|
|
173
|
+
).rejects.toThrow(ForbiddenException);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('bloqueia POST /auth/logout, que é @Public()', async () => {
|
|
177
|
+
// O cookie `rt` da mesma origem é o do OPERADOR: sair por aqui revogaria
|
|
178
|
+
// a sessão real dele.
|
|
179
|
+
const { guard, deps } = makeService();
|
|
180
|
+
setMeta(deps.reflector, { public: true });
|
|
181
|
+
setPath(deps.reflector, 'auth', 'logout', RequestMethod.POST);
|
|
182
|
+
|
|
183
|
+
await expect(
|
|
184
|
+
guard.canActivate(makeContext(impersonatedRequest) as any),
|
|
185
|
+
).rejects.toThrow(ForbiddenException);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('bloqueia a família /auth/pairing por prefixo', async () => {
|
|
189
|
+
const { guard, deps } = makeService();
|
|
190
|
+
setMeta(deps.reflector, { public: true });
|
|
191
|
+
setPath(deps.reflector, 'auth', 'pairing/redeem', RequestMethod.POST);
|
|
192
|
+
|
|
193
|
+
await expect(
|
|
194
|
+
guard.canActivate(makeContext(impersonatedRequest) as any),
|
|
195
|
+
).rejects.toThrow(ForbiddenException);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('bloqueia simulação aninhada em POST /impersonation/start', async () => {
|
|
199
|
+
const { guard, deps } = makeService();
|
|
200
|
+
setMeta(deps.reflector, { withRole: true });
|
|
201
|
+
setPath(deps.reflector, 'impersonation', 'start', RequestMethod.POST);
|
|
202
|
+
|
|
203
|
+
await expect(
|
|
204
|
+
guard.canActivate(makeContext(impersonatedRequest) as any),
|
|
205
|
+
).rejects.toThrow(ForbiddenException);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('libera POST /impersonation/stop — sem ela não há como sair', async () => {
|
|
209
|
+
const { guard, deps } = makeService();
|
|
210
|
+
setMeta(deps.reflector, { withNoRole: true });
|
|
211
|
+
setPath(deps.reflector, 'impersonation', 'stop', RequestMethod.POST);
|
|
212
|
+
|
|
213
|
+
await expect(
|
|
214
|
+
guard.canActivate(makeContext(impersonatedRequest) as any),
|
|
215
|
+
).resolves.toBe(true);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('não interfere fora de uma simulação', async () => {
|
|
219
|
+
const { guard, deps } = makeService();
|
|
220
|
+
setMeta(deps.reflector, { withNoRole: true });
|
|
221
|
+
setPath(deps.reflector, 'auth', 'desktop-token', RequestMethod.GET);
|
|
222
|
+
const request = {
|
|
223
|
+
headers: { authorization: 'Bearer tok' },
|
|
224
|
+
auth: { sub: 3, sessionId: 1 },
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
await expect(guard.canActivate(makeContext(request) as any)).resolves.toBe(
|
|
228
|
+
true,
|
|
229
|
+
);
|
|
230
|
+
});
|
|
231
|
+
});
|
|
157
232
|
});
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
import { METHOD_METADATA } from '@nestjs/common/constants';
|
|
15
15
|
import { Reflector } from '@nestjs/core';
|
|
16
16
|
import { Request } from 'express';
|
|
17
|
+
import { isRestrictedDuringImpersonation } from '../../impersonation/impersonation-restrictions';
|
|
17
18
|
|
|
18
19
|
@Injectable()
|
|
19
20
|
export class RoleGuard implements CanActivate {
|
|
@@ -29,6 +30,27 @@ export class RoleGuard implements CanActivate {
|
|
|
29
30
|
) {}
|
|
30
31
|
|
|
31
32
|
async canActivate(context: ExecutionContext): Promise<boolean> {
|
|
33
|
+
const request = context.switchToHttp().getRequest<Request>();
|
|
34
|
+
const { fullPath, httpMethod } = this.resolveRoute(context);
|
|
35
|
+
|
|
36
|
+
// ANTES dos atalhos de @Public()/@NoRole(): sao esses dois decorators que
|
|
37
|
+
// protegem as rotas que uma simulacao nao pode alcancar (ver
|
|
38
|
+
// impersonation-restrictions.ts). Checar depois deles deixaria
|
|
39
|
+
// /auth/desktop-token e /auth/logout passarem.
|
|
40
|
+
if (
|
|
41
|
+
(request as any)?.auth?.imp === true &&
|
|
42
|
+
isRestrictedDuringImpersonation(httpMethod, fullPath)
|
|
43
|
+
) {
|
|
44
|
+
const locale = request['locale'] || 'en';
|
|
45
|
+
throw new ForbiddenException(
|
|
46
|
+
getLocaleText(
|
|
47
|
+
'impersonation.restrictedRoute',
|
|
48
|
+
locale,
|
|
49
|
+
'This action is not available during an access simulation.',
|
|
50
|
+
),
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
32
54
|
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
|
|
33
55
|
context.getHandler(),
|
|
34
56
|
context.getClass(),
|
|
@@ -57,23 +79,6 @@ export class RoleGuard implements CanActivate {
|
|
|
57
79
|
return true;
|
|
58
80
|
}
|
|
59
81
|
|
|
60
|
-
const request = context.switchToHttp().getRequest<Request>();
|
|
61
|
-
const controller = context.getClass();
|
|
62
|
-
const handler = context.getHandler();
|
|
63
|
-
const controllerPath = this.reflector.get<string>('path', controller) || '';
|
|
64
|
-
const methodPath = this.reflector.get<string>('path', handler) || '';
|
|
65
|
-
|
|
66
|
-
const requestMethod = this.reflector.get<RequestMethod>(
|
|
67
|
-
METHOD_METADATA,
|
|
68
|
-
handler,
|
|
69
|
-
);
|
|
70
|
-
|
|
71
|
-
let fullPath = `/${controllerPath}/${methodPath}`.replace(/\/+/g, '/');
|
|
72
|
-
|
|
73
|
-
if (fullPath.endsWith('/')) {
|
|
74
|
-
fullPath = fullPath.slice(0, -1);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
82
|
const token = this.extractTokenFromHeader(request);
|
|
78
83
|
|
|
79
84
|
if (!token) {
|
|
@@ -85,37 +90,11 @@ export class RoleGuard implements CanActivate {
|
|
|
85
90
|
|
|
86
91
|
const userId = (request as any)?.auth?.sub;
|
|
87
92
|
|
|
88
|
-
let httpMethod: any;
|
|
89
|
-
switch (requestMethod) {
|
|
90
|
-
case RequestMethod.GET:
|
|
91
|
-
httpMethod = 'GET';
|
|
92
|
-
break;
|
|
93
|
-
case RequestMethod.POST:
|
|
94
|
-
httpMethod = 'POST';
|
|
95
|
-
break;
|
|
96
|
-
case RequestMethod.PUT:
|
|
97
|
-
httpMethod = 'PUT';
|
|
98
|
-
break;
|
|
99
|
-
case RequestMethod.DELETE:
|
|
100
|
-
httpMethod = 'DELETE';
|
|
101
|
-
break;
|
|
102
|
-
case RequestMethod.PATCH:
|
|
103
|
-
httpMethod = 'PATCH';
|
|
104
|
-
break;
|
|
105
|
-
case RequestMethod.OPTIONS:
|
|
106
|
-
httpMethod = 'OPTIONS';
|
|
107
|
-
break;
|
|
108
|
-
case RequestMethod.HEAD:
|
|
109
|
-
httpMethod = 'HEAD';
|
|
110
|
-
break;
|
|
111
|
-
case RequestMethod.ALL:
|
|
112
|
-
httpMethod = 'ALL';
|
|
113
|
-
break;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
93
|
const route = await this.prisma.route.count({
|
|
117
94
|
where: {
|
|
118
|
-
method
|
|
95
|
+
// `route.method` e um enum gerado com nome variavel por instalacao; o
|
|
96
|
+
// valor vem dos metadados do Nest e sempre casa com um dos membros.
|
|
97
|
+
method: httpMethod as any,
|
|
119
98
|
url: fullPath,
|
|
120
99
|
role_route: {
|
|
121
100
|
some: {
|
|
@@ -171,6 +150,45 @@ export class RoleGuard implements CanActivate {
|
|
|
171
150
|
return true;
|
|
172
151
|
}
|
|
173
152
|
|
|
153
|
+
/**
|
|
154
|
+
* Reconstroi `METHOD /path` a partir dos metadados do Nest — a mesma chave
|
|
155
|
+
* usada na tabela `route`, com os `:params` declarados no controller.
|
|
156
|
+
*/
|
|
157
|
+
private resolveRoute(context: ExecutionContext): {
|
|
158
|
+
fullPath: string;
|
|
159
|
+
httpMethod: string;
|
|
160
|
+
} {
|
|
161
|
+
const controllerPath =
|
|
162
|
+
this.reflector.get<string>('path', context.getClass()) || '';
|
|
163
|
+
const methodPath =
|
|
164
|
+
this.reflector.get<string>('path', context.getHandler()) || '';
|
|
165
|
+
|
|
166
|
+
let fullPath = `/${controllerPath}/${methodPath}`.replace(/\/+/g, '/');
|
|
167
|
+
|
|
168
|
+
if (fullPath.endsWith('/')) {
|
|
169
|
+
fullPath = fullPath.slice(0, -1);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const requestMethod = this.reflector.get<RequestMethod>(
|
|
173
|
+
METHOD_METADATA,
|
|
174
|
+
context.getHandler(),
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
const httpMethod =
|
|
178
|
+
{
|
|
179
|
+
[RequestMethod.GET]: 'GET',
|
|
180
|
+
[RequestMethod.POST]: 'POST',
|
|
181
|
+
[RequestMethod.PUT]: 'PUT',
|
|
182
|
+
[RequestMethod.DELETE]: 'DELETE',
|
|
183
|
+
[RequestMethod.PATCH]: 'PATCH',
|
|
184
|
+
[RequestMethod.OPTIONS]: 'OPTIONS',
|
|
185
|
+
[RequestMethod.HEAD]: 'HEAD',
|
|
186
|
+
[RequestMethod.ALL]: 'ALL',
|
|
187
|
+
}[requestMethod] ?? '';
|
|
188
|
+
|
|
189
|
+
return { fullPath, httpMethod };
|
|
190
|
+
}
|
|
191
|
+
|
|
174
192
|
private extractTokenFromHeader(request: Request): string | undefined {
|
|
175
193
|
const [type, token] = request.headers.authorization?.split(' ') ?? [];
|
|
176
194
|
return type === 'Bearer' ? token : undefined;
|
|
@@ -292,6 +292,9 @@ describe('SessionService', () => {
|
|
|
292
292
|
revoked_at: null,
|
|
293
293
|
expires_at: new Date(Date.now() + 100000),
|
|
294
294
|
browser_id: 'br',
|
|
295
|
+
// O refresh e fail-closed: `impersonator_user_id` precisa vir do select
|
|
296
|
+
// e ser explicitamente null para a rotacao seguir.
|
|
297
|
+
impersonator_user_id: null,
|
|
295
298
|
});
|
|
296
299
|
const tx = {
|
|
297
300
|
user_session: {
|
|
@@ -312,6 +315,84 @@ describe('SessionService', () => {
|
|
|
312
315
|
});
|
|
313
316
|
});
|
|
314
317
|
|
|
318
|
+
describe('createImpersonation', () => {
|
|
319
|
+
it('grava o operador, expiração própria e browser_id nulo', async () => {
|
|
320
|
+
jest.useFakeTimers().setSystemTime(new Date('2026-08-22T12:00:00Z'));
|
|
321
|
+
const { service, deps } = makeService();
|
|
322
|
+
deps.prisma.user_session.create.mockResolvedValue({ id: 9 });
|
|
323
|
+
|
|
324
|
+
await service.createImpersonation('pt', 42, 7, '1.2.3.4', 'ua', 30, 'chamado 123');
|
|
325
|
+
|
|
326
|
+
const data = (deps.prisma.user_session.create as any).mock.calls[0][0].data;
|
|
327
|
+
expect(data.user_id).toBe(42);
|
|
328
|
+
expect(data.impersonator_user_id).toBe(7);
|
|
329
|
+
expect(data.impersonation_reason).toBe('chamado 123');
|
|
330
|
+
expect(data.browser_id).toBeNull();
|
|
331
|
+
expect(data.expires_at).toEqual(new Date('2026-08-22T12:30:00Z'));
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it('descarta o token opaco — não existe refresh token para a sessão simulada', async () => {
|
|
335
|
+
const { service, deps } = makeService();
|
|
336
|
+
deps.prisma.user_session.create.mockResolvedValue({ id: 9 });
|
|
337
|
+
|
|
338
|
+
const result: any = await service.createImpersonation(
|
|
339
|
+
'pt', 42, 7, '1.2.3.4', 'ua', 30,
|
|
340
|
+
);
|
|
341
|
+
|
|
342
|
+
expect(result).toEqual({ id: 9 });
|
|
343
|
+
expect(result.token).toBeUndefined();
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
it('NÃO aplica o limite de sessões: simular alguém não pode derrubar a pessoa', async () => {
|
|
347
|
+
const { service, deps } = makeService();
|
|
348
|
+
deps.setting.getSettingValues.mockResolvedValue({ 'max-concurrent-sessions': 1 });
|
|
349
|
+
deps.prisma.user_session.create.mockResolvedValue({ id: 9 });
|
|
350
|
+
|
|
351
|
+
await service.createImpersonation('pt', 42, 7, '1.2.3.4', 'ua', 30);
|
|
352
|
+
|
|
353
|
+
expect(deps.prisma.user_session.findMany).not.toHaveBeenCalled();
|
|
354
|
+
expect(deps.prisma.user_session.updateMany).not.toHaveBeenCalled();
|
|
355
|
+
});
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
describe('refresh - sessão simulada', () => {
|
|
359
|
+
it('rejeita rotação de sessão de simulação', async () => {
|
|
360
|
+
const { service, deps } = makeService();
|
|
361
|
+
deps.prisma.user_session.findFirst.mockResolvedValue({
|
|
362
|
+
id: 1,
|
|
363
|
+
user_id: 42,
|
|
364
|
+
ip_address: '1.2.3.4',
|
|
365
|
+
user_agent: 'ua',
|
|
366
|
+
revoked_at: null,
|
|
367
|
+
expires_at: new Date(Date.now() + 100000),
|
|
368
|
+
browser_id: null,
|
|
369
|
+
impersonator_user_id: 7,
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
await expect(service.refresh('pt', 'tok', '1.2.3.4', 'ua')).rejects.toThrow(
|
|
373
|
+
UnauthorizedException,
|
|
374
|
+
);
|
|
375
|
+
expect(deps.prisma.$transaction).not.toHaveBeenCalled();
|
|
376
|
+
});
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
describe('enforceSessionLimit - sessões simuladas', () => {
|
|
380
|
+
it('não conta simulações no limite de sessões do usuário', async () => {
|
|
381
|
+
const { service, deps } = makeService();
|
|
382
|
+
deps.setting.getSettingValues.mockResolvedValue({
|
|
383
|
+
'max-concurrent-sessions': 2,
|
|
384
|
+
'refresh-token-expiration-minutes': 100,
|
|
385
|
+
});
|
|
386
|
+
deps.prisma.user_session.findMany.mockResolvedValue([]);
|
|
387
|
+
deps.prisma.user_session.create.mockResolvedValue({ id: 1 });
|
|
388
|
+
|
|
389
|
+
await service.create('pt', 42, '1.2.3.4', 'ua', 'br');
|
|
390
|
+
|
|
391
|
+
const where = (deps.prisma.user_session.findMany as any).mock.calls[0][0].where;
|
|
392
|
+
expect(where.impersonator_user_id).toBeNull();
|
|
393
|
+
});
|
|
394
|
+
});
|
|
395
|
+
|
|
315
396
|
describe('getUserSessions - paginação', () => {
|
|
316
397
|
it('lança BadRequest quando usuário não existe', async () => {
|
|
317
398
|
const { service, deps } = makeService();
|
|
@@ -79,6 +79,77 @@ export class SessionService {
|
|
|
79
79
|
return { token, session };
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Sessao de simulacao de acesso: um operador vendo o sistema como outro
|
|
84
|
+
* usuario. Caminho separado do `create` por tres motivos, todos deliberados:
|
|
85
|
+
*
|
|
86
|
+
* 1. NAO chama `enforceSessionLimit`. Com `max-concurrent-sessions > 0`,
|
|
87
|
+
* abrir uma simulacao despejaria sessoes REAIS do alvo — simular alguem
|
|
88
|
+
* derrubaria a pessoa.
|
|
89
|
+
* 2. O token opaco e gerado e DESCARTADO. Ninguem recebe refresh token para
|
|
90
|
+
* esta sessao, entao ela e nao renovavel por construcao, nao so por regra.
|
|
91
|
+
* 3. `browser_id` fica nulo, para a simulacao nao se misturar aos slots de
|
|
92
|
+
* navegador do alvo.
|
|
93
|
+
*/
|
|
94
|
+
async createImpersonation(
|
|
95
|
+
locale: string,
|
|
96
|
+
targetUserId: number,
|
|
97
|
+
impersonatorUserId: number,
|
|
98
|
+
ipAddress: string,
|
|
99
|
+
userAgent: string,
|
|
100
|
+
expiresInMinutes: number,
|
|
101
|
+
reason?: string | null,
|
|
102
|
+
) {
|
|
103
|
+
if (!targetUserId || !impersonatorUserId) {
|
|
104
|
+
throw new BadRequestException(getLocaleText('session.userIdRequired', locale, 'User ID is required to create a session.'));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const throwawayToken = await this.token.createOpaqueToken();
|
|
108
|
+
|
|
109
|
+
return this.prisma.user_session.create({
|
|
110
|
+
data: {
|
|
111
|
+
user_id: targetUserId,
|
|
112
|
+
ip_address: ipAddress || 'unknown',
|
|
113
|
+
user_agent: userAgent || 'unknown',
|
|
114
|
+
expires_at: new Date(Date.now() + expiresInMinutes * 60 * 1000),
|
|
115
|
+
hash: this.security.hashWithPepper(throwawayToken),
|
|
116
|
+
browser_id: null,
|
|
117
|
+
impersonator_user_id: impersonatorUserId,
|
|
118
|
+
impersonation_reason: reason ?? null,
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Anexa `impersonator: { id, name }` as sessoes simuladas. Nao ha relacao
|
|
125
|
+
* Prisma para isso: `impersonator_user_id` e INT puro, sem FK, para nao
|
|
126
|
+
* renomear `user.user_session` no db pull. Mesmo padrao de
|
|
127
|
+
* `UserService.listUserChangeLogs` com os atores.
|
|
128
|
+
*/
|
|
129
|
+
private async attachImpersonators<T extends { impersonator_user_id?: number | null }>(rows: T[]) {
|
|
130
|
+
const ids = Array.from(
|
|
131
|
+
new Set(rows.map((row) => row.impersonator_user_id).filter((id): id is number => typeof id === 'number')),
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
if (!ids.length) {
|
|
135
|
+
return rows.map((row) => ({ ...row, impersonator: null }));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const users = await this.prisma.user.findMany({
|
|
139
|
+
where: { id: { in: ids } },
|
|
140
|
+
select: { id: true, name: true },
|
|
141
|
+
});
|
|
142
|
+
const byId = new Map(users.map((user) => [user.id, user]));
|
|
143
|
+
|
|
144
|
+
return rows.map((row) => ({
|
|
145
|
+
...row,
|
|
146
|
+
impersonator:
|
|
147
|
+
typeof row.impersonator_user_id === 'number'
|
|
148
|
+
? (byId.get(row.impersonator_user_id) ?? { id: row.impersonator_user_id, name: '' })
|
|
149
|
+
: null,
|
|
150
|
+
}));
|
|
151
|
+
}
|
|
152
|
+
|
|
82
153
|
async refresh(locale: string, refreshToken: string, ipAddress: string, userAgent: string, browserId?: string | null) {
|
|
83
154
|
if (!refreshToken) {
|
|
84
155
|
throw new UnauthorizedException(getLocaleText('accessDenied', locale, 'Access denied.'));
|
|
@@ -88,7 +159,7 @@ export class SessionService {
|
|
|
88
159
|
|
|
89
160
|
const rawSession = await this.prisma.user_session.findFirst({
|
|
90
161
|
where: { hash },
|
|
91
|
-
select: { id: true, user_id: true, ip_address: true, user_agent: true, revoked_at: true, expires_at: true, browser_id: true },
|
|
162
|
+
select: { id: true, user_id: true, ip_address: true, user_agent: true, revoked_at: true, expires_at: true, browser_id: true, impersonator_user_id: true },
|
|
92
163
|
});
|
|
93
164
|
|
|
94
165
|
if (!rawSession) {
|
|
@@ -106,6 +177,15 @@ export class SessionService {
|
|
|
106
177
|
throw new UnauthorizedException(getLocaleText('accessDenied', locale, 'Access denied.'));
|
|
107
178
|
}
|
|
108
179
|
|
|
180
|
+
// Simulacao de acesso e nao renovavel. `createImpersonation` ja descarta o
|
|
181
|
+
// token opaco, entao chegar aqui significa que alguem obteve o segredo por
|
|
182
|
+
// outro caminho — e renovar transformaria a janela de 30 minutos numa sessao
|
|
183
|
+
// normal de 30 dias, sem a marca de simulado.
|
|
184
|
+
if (rawSession.impersonator_user_id !== null) {
|
|
185
|
+
this.logger.warn(`[SessionRefresh] Refresh rejected: sessionId=${rawSession.id} userId=${rawSession.user_id} is an impersonation session`);
|
|
186
|
+
throw new UnauthorizedException(getLocaleText('accessDenied', locale, 'Access denied.'));
|
|
187
|
+
}
|
|
188
|
+
|
|
109
189
|
const session = {
|
|
110
190
|
id: rawSession.id,
|
|
111
191
|
user_id: rawSession.user_id,
|
|
@@ -223,7 +303,9 @@ export class SessionService {
|
|
|
223
303
|
});
|
|
224
304
|
|
|
225
305
|
const itemsWithLocation = await Promise.all(
|
|
226
|
-
|
|
306
|
+
// O `hash` é o resumo do refresh token: nenhum cliente precisa dele, e
|
|
307
|
+
// a lista de sessões é exibida na tela de segurança do app e do site.
|
|
308
|
+
paginate.data.map(async ({ hash: _hash, ...s }) => {
|
|
227
309
|
const ip = s.ip_address || s.ip || null;
|
|
228
310
|
let location: GeoIpResult | null = null;
|
|
229
311
|
if (ip && ip !== '127.0.0.1' && ip !== '::1') {
|
|
@@ -240,7 +322,7 @@ export class SessionService {
|
|
|
240
322
|
);
|
|
241
323
|
|
|
242
324
|
return {
|
|
243
|
-
data: itemsWithLocation,
|
|
325
|
+
data: await this.attachImpersonators(itemsWithLocation),
|
|
244
326
|
total: paginate.total || 0,
|
|
245
327
|
lastPage: Math.ceil((paginate.total || 0) / (paginate.pageSize || 1)),
|
|
246
328
|
page: paginate.page ?? 1,
|
|
@@ -272,7 +354,9 @@ export class SessionService {
|
|
|
272
354
|
});
|
|
273
355
|
|
|
274
356
|
const itemsWithLocation = await Promise.all(
|
|
275
|
-
|
|
357
|
+
// O `hash` é o resumo do refresh token: nenhum cliente precisa dele, e
|
|
358
|
+
// a lista de sessões é exibida na tela de segurança do app e do site.
|
|
359
|
+
paginate.data.map(async ({ hash: _hash, ...s }) => {
|
|
276
360
|
const ip = s.ip_address || s.ip || null;
|
|
277
361
|
let location: GeoIpResult | null = null;
|
|
278
362
|
if (ip && ip !== '127.0.0.1' && ip !== '::1') {
|
|
@@ -289,7 +373,7 @@ export class SessionService {
|
|
|
289
373
|
);
|
|
290
374
|
|
|
291
375
|
return {
|
|
292
|
-
data: itemsWithLocation,
|
|
376
|
+
data: await this.attachImpersonators(itemsWithLocation),
|
|
293
377
|
total: paginate.total || 0,
|
|
294
378
|
lastPage: Math.ceil((paginate.total || 0) / (paginate.pageSize || 1)),
|
|
295
379
|
page: paginate.page ?? 1,
|
|
@@ -391,7 +475,10 @@ export class SessionService {
|
|
|
391
475
|
where: {
|
|
392
476
|
user_id: userId,
|
|
393
477
|
revoked_at: null,
|
|
394
|
-
expires_at: { gt: new Date() }
|
|
478
|
+
expires_at: { gt: new Date() },
|
|
479
|
+
// Simulacao de acesso nao consome slot: ela nasce com browser_id nulo e
|
|
480
|
+
// seria contada como slot proprio, despejando uma sessao real do alvo.
|
|
481
|
+
impersonator_user_id: null,
|
|
395
482
|
},
|
|
396
483
|
orderBy: { created_at: 'asc' },
|
|
397
484
|
select: { id: true, browser_id: true }
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BrandAssetBases,
|
|
3
|
+
isPublicAssetUrl,
|
|
4
|
+
resolveBrandAssetUrl,
|
|
5
|
+
} from '@hed-hog/api-mail';
|
|
6
|
+
import { forwardRef, Inject, Injectable, Logger } from '@nestjs/common';
|
|
7
|
+
import { ConfigService } from '@nestjs/config';
|
|
8
|
+
import { SettingService } from './setting.service';
|
|
9
|
+
|
|
10
|
+
// Reexportados para que as libraries que ja dependem do core (lms, crm,
|
|
11
|
+
// operations) usem a regra sem precisar declarar `@hed-hog/api-mail`.
|
|
12
|
+
export { isPublicAssetUrl, resolveBrandAssetUrl };
|
|
13
|
+
export type { BrandAssetBases };
|
|
14
|
+
|
|
15
|
+
/** Timeout do fetch em {@link BrandAssetService.fetchAsDataUri}. */
|
|
16
|
+
const FETCH_TIMEOUT_MS = 5_000;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Resolucao das settings de marca (`image-url`, `icon-url`, `pdf-logo`) para
|
|
20
|
+
* URL absoluta, para quem renderiza fora do browser: e-mail, PDF, relatorio.
|
|
21
|
+
*
|
|
22
|
+
* Existe porque a mesma regra estava duplicada em quatro lugares, todos
|
|
23
|
+
* derivando a base da env `API_URL` - que nao esta definida em nenhum
|
|
24
|
+
* manifesto de producao. As settings `url` e `api-url` sao a fonte de verdade
|
|
25
|
+
* (gravadas na instalacao e editaveis no admin); a env fica como fallback de
|
|
26
|
+
* dev/CI.
|
|
27
|
+
*/
|
|
28
|
+
@Injectable()
|
|
29
|
+
export class BrandAssetService {
|
|
30
|
+
private readonly logger = new Logger(BrandAssetService.name);
|
|
31
|
+
|
|
32
|
+
constructor(
|
|
33
|
+
@Inject(forwardRef(() => SettingService))
|
|
34
|
+
private readonly setting: SettingService,
|
|
35
|
+
private readonly configService: ConfigService,
|
|
36
|
+
) {}
|
|
37
|
+
|
|
38
|
+
/** Bases publicas do front (setting `url`) e da API (setting `api-url`). */
|
|
39
|
+
async getBases(): Promise<BrandAssetBases> {
|
|
40
|
+
const port = this.configService.get<string | number>('PORT') ?? 3100;
|
|
41
|
+
let settings: Record<string, any> = {};
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
settings = await this.setting.getSettingValues(['url', 'api-url']);
|
|
45
|
+
} catch (error) {
|
|
46
|
+
this.logger.warn(
|
|
47
|
+
`Could not read the base URL settings, falling back to the environment: ${
|
|
48
|
+
error instanceof Error ? error.message : String(error)
|
|
49
|
+
}`,
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const read = (slug: string) => String(settings[slug] ?? '').trim();
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
appBaseUrl: read('url'),
|
|
57
|
+
apiBaseUrl:
|
|
58
|
+
read('api-url') ||
|
|
59
|
+
String(this.configService.get<string>('API_URL') ?? '').trim() ||
|
|
60
|
+
`http://localhost:${port}`,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Resolve um valor de setting de marca para URL absoluta. Devolve `''`
|
|
66
|
+
* quando nao ha valor ou nao ha base utilizavel - quem chama decide o
|
|
67
|
+
* fallback.
|
|
68
|
+
*/
|
|
69
|
+
async resolveAssetUrl(value?: string | null): Promise<string> {
|
|
70
|
+
const trimmed = String(value ?? '').trim();
|
|
71
|
+
|
|
72
|
+
if (!trimmed) {
|
|
73
|
+
return '';
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return resolveBrandAssetUrl(trimmed, await this.getBases());
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Logo do sistema ja absoluto, na ordem de prioridade informada.
|
|
81
|
+
*
|
|
82
|
+
* `slugs` permite ao chamador antepor um slug proprio (ex.: `pdf-logo`)
|
|
83
|
+
* antes de cair no `image-url` global.
|
|
84
|
+
*/
|
|
85
|
+
async resolveLogoUrl(
|
|
86
|
+
slugs: string[] = ['image-url', 'icon-url'],
|
|
87
|
+
): Promise<string> {
|
|
88
|
+
const settings = await this.setting.getSettingValues(slugs);
|
|
89
|
+
|
|
90
|
+
for (const slug of slugs) {
|
|
91
|
+
const value = String(settings[slug] ?? '').trim();
|
|
92
|
+
|
|
93
|
+
if (value) {
|
|
94
|
+
return this.resolveAssetUrl(value);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return '';
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Baixa a imagem e devolve como data URI.
|
|
103
|
+
*
|
|
104
|
+
* O Playwright renderiza o PDF em uma pagina sem sessao e, dependendo do
|
|
105
|
+
* ambiente, sem rota para o host publico; embutir o binario evita o slot de
|
|
106
|
+
* imagem vazio. Devolve `null` em qualquer falha, para o chamador cair na
|
|
107
|
+
* URL absoluta.
|
|
108
|
+
*/
|
|
109
|
+
async fetchAsDataUri(url: string): Promise<string | null> {
|
|
110
|
+
if (!url || url.startsWith('data:')) {
|
|
111
|
+
return url || null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
const controller = new AbortController();
|
|
116
|
+
const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
117
|
+
const response = await globalThis.fetch(url, { signal: controller.signal });
|
|
118
|
+
clearTimeout(timeout);
|
|
119
|
+
|
|
120
|
+
if (!response.ok) {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const contentType = response.headers.get('content-type') ?? 'image/svg+xml';
|
|
125
|
+
const buffer = await response.arrayBuffer();
|
|
126
|
+
|
|
127
|
+
return `data:${contentType};base64,${Buffer.from(buffer).toString('base64')}`;
|
|
128
|
+
} catch {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Reexportado para quem renderiza e-mail. Ver `isPublicAssetUrl`. */
|
|
134
|
+
isPublicUrl(url?: string | null): boolean {
|
|
135
|
+
return isPublicAssetUrl(url);
|
|
136
|
+
}
|
|
137
|
+
}
|