@open-mercato/core 0.6.7-develop.6795.1.8a3f27921c → 0.6.7-develop.6814.1.0627c7e9f1
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 +1 -1
- package/dist/generated/entities/customer_deal_person_link/index.js +2 -0
- package/dist/generated/entities/customer_deal_person_link/index.js.map +2 -2
- package/dist/generated/entities/customer_user_invitation/index.js +2 -0
- package/dist/generated/entities/customer_user_invitation/index.js.map +2 -2
- package/dist/generated/entity-fields-registry.js +2 -0
- package/dist/generated/entity-fields-registry.js.map +2 -2
- package/dist/modules/business_rules/api/openmercato-call-options/route.js +85 -0
- package/dist/modules/business_rules/api/openmercato-call-options/route.js.map +7 -0
- package/dist/modules/business_rules/api/rules/route.js +71 -4
- package/dist/modules/business_rules/api/rules/route.js.map +2 -2
- package/dist/modules/business_rules/components/ActionBuilder.js +47 -4
- package/dist/modules/business_rules/components/ActionBuilder.js.map +3 -3
- package/dist/modules/business_rules/components/ActionRow.js +106 -16
- package/dist/modules/business_rules/components/ActionRow.js.map +2 -2
- package/dist/modules/business_rules/components/utils/actionValidation.js +6 -0
- package/dist/modules/business_rules/components/utils/actionValidation.js.map +2 -2
- package/dist/modules/business_rules/lib/action-executor.js +148 -0
- package/dist/modules/business_rules/lib/action-executor.js.map +2 -2
- package/dist/modules/business_rules/lib/openmercato-call-options-types.js +5 -0
- package/dist/modules/business_rules/lib/openmercato-call-options-types.js.map +7 -0
- package/dist/modules/business_rules/lib/openmercato-call-options.js +231 -0
- package/dist/modules/business_rules/lib/openmercato-call-options.js.map +7 -0
- package/dist/modules/business_rules/lib/rule-engine.js +5 -1
- package/dist/modules/business_rules/lib/rule-engine.js.map +2 -2
- package/dist/modules/communication_channels/api/post/webhook/[provider]/route.js +11 -1
- package/dist/modules/communication_channels/api/post/webhook/[provider]/route.js.map +2 -2
- package/dist/modules/communication_channels/api/post/webhooks/gmail/route.js +11 -6
- package/dist/modules/communication_channels/api/post/webhooks/gmail/route.js.map +2 -2
- package/dist/modules/configs/lib/upgrade-actions.js +12 -0
- package/dist/modules/configs/lib/upgrade-actions.js.map +2 -2
- package/dist/modules/customer_accounts/api/admin/users/[id].js +10 -1
- package/dist/modules/customer_accounts/api/admin/users/[id].js.map +2 -2
- package/dist/modules/customer_accounts/api/admin/users-invite.js +61 -4
- package/dist/modules/customer_accounts/api/admin/users-invite.js.map +2 -2
- package/dist/modules/customer_accounts/api/portal/users-invite.js +20 -2
- package/dist/modules/customer_accounts/api/portal/users-invite.js.map +2 -2
- package/dist/modules/customer_accounts/data/entities.js +3 -0
- package/dist/modules/customer_accounts/data/entities.js.map +2 -2
- package/dist/modules/customer_accounts/data/validators.js +1 -0
- package/dist/modules/customer_accounts/data/validators.js.map +2 -2
- package/dist/modules/customer_accounts/emails/CustomerInvitationEmail.js +26 -0
- package/dist/modules/customer_accounts/emails/CustomerInvitationEmail.js.map +7 -0
- package/dist/modules/customer_accounts/lib/customerEntityOwnership.js +29 -3
- package/dist/modules/customer_accounts/lib/customerEntityOwnership.js.map +2 -2
- package/dist/modules/customer_accounts/lib/customerUrl.js +19 -10
- package/dist/modules/customer_accounts/lib/customerUrl.js.map +2 -2
- package/dist/modules/customer_accounts/lib/invitationEmail.js +35 -0
- package/dist/modules/customer_accounts/lib/invitationEmail.js.map +7 -0
- package/dist/modules/customer_accounts/migrations/Migration20260723120000_customer_accounts.js +13 -0
- package/dist/modules/customer_accounts/migrations/Migration20260723120000_customer_accounts.js.map +7 -0
- package/dist/modules/customer_accounts/services/customerInvitationService.js +32 -2
- package/dist/modules/customer_accounts/services/customerInvitationService.js.map +2 -2
- package/dist/modules/customer_accounts/subscribers/autoLinkCrm.js +47 -7
- package/dist/modules/customer_accounts/subscribers/autoLinkCrm.js.map +2 -2
- package/dist/modules/customer_accounts/widgets/injection/account-status/widget.client.js +2 -2
- package/dist/modules/customer_accounts/widgets/injection/account-status/widget.client.js.map +2 -2
- package/dist/modules/customer_accounts/widgets/injection-table.js +1 -1
- package/dist/modules/customer_accounts/widgets/injection-table.js.map +2 -2
- package/dist/modules/customers/api/deals/[id]/people/route.js +4 -2
- package/dist/modules/customers/api/deals/[id]/people/route.js.map +2 -2
- package/dist/modules/customers/api/deals/[id]/route.js +7 -3
- package/dist/modules/customers/api/deals/[id]/route.js.map +2 -2
- package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js +11 -1
- package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/[id]/page.js +14 -8
- package/dist/modules/customers/backend/customers/deals/[id]/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/people-v2/[id]/page.js +21 -6
- package/dist/modules/customers/backend/customers/people-v2/[id]/page.js.map +2 -2
- package/dist/modules/customers/commands/deals.js +48 -10
- package/dist/modules/customers/commands/deals.js.map +2 -2
- package/dist/modules/customers/commands/people.js +2 -0
- package/dist/modules/customers/commands/people.js.map +2 -2
- package/dist/modules/customers/components/detail/CompanyDetailTabs.js +26 -6
- package/dist/modules/customers/components/detail/CompanyDetailTabs.js.map +2 -2
- package/dist/modules/customers/components/detail/DealDetailTabs.js +30 -8
- package/dist/modules/customers/components/detail/DealDetailTabs.js.map +2 -2
- package/dist/modules/customers/components/detail/PersonDetailTabs.js +30 -8
- package/dist/modules/customers/components/detail/PersonDetailTabs.js.map +2 -2
- package/dist/modules/customers/data/entities.js +8 -0
- package/dist/modules/customers/data/entities.js.map +2 -2
- package/dist/modules/customers/data/validators.js +2 -1
- package/dist/modules/customers/data/validators.js.map +2 -2
- package/dist/modules/customers/migrations/Migration20260723210000_customers.js +15 -0
- package/dist/modules/customers/migrations/Migration20260723210000_customers.js.map +7 -0
- package/dist/modules/entities/api/entity-settings.js +59 -9
- package/dist/modules/entities/api/entity-settings.js.map +2 -2
- package/dist/modules/entities/backend/config/settings/page.js +10 -0
- package/dist/modules/entities/backend/config/settings/page.js.map +7 -0
- package/dist/modules/entities/backend/config/settings/page.meta.js +36 -0
- package/dist/modules/entities/backend/config/settings/page.meta.js.map +7 -0
- package/dist/modules/entities/backend/entities/user/create/page.js +16 -7
- package/dist/modules/entities/backend/entities/user/create/page.js.map +2 -2
- package/dist/modules/entities/components/EntitySettingsManager.js +136 -0
- package/dist/modules/entities/components/EntitySettingsManager.js.map +7 -0
- package/dist/modules/inbox_ops/api/webhook/inbound.js +14 -10
- package/dist/modules/inbox_ops/api/webhook/inbound.js.map +2 -2
- package/dist/modules/payment_gateways/api/webhook/[provider]/route.js +11 -1
- package/dist/modules/payment_gateways/api/webhook/[provider]/route.js.map +2 -2
- package/dist/modules/payment_gateways/data/entities.js +4 -0
- package/dist/modules/payment_gateways/data/entities.js.map +2 -2
- package/dist/modules/payment_gateways/lib/gateway-service.js +12 -2
- package/dist/modules/payment_gateways/lib/gateway-service.js.map +2 -2
- package/dist/modules/payment_gateways/lib/session-idempotency.js +33 -0
- package/dist/modules/payment_gateways/lib/session-idempotency.js.map +2 -2
- package/dist/modules/payment_gateways/migrations/Migration20260803103035_payment_gateways.js +18 -0
- package/dist/modules/payment_gateways/migrations/Migration20260803103035_payment_gateways.js.map +7 -0
- package/dist/modules/payment_gateways/setup.js +47 -0
- package/dist/modules/payment_gateways/setup.js.map +2 -2
- package/dist/modules/payment_gateways/workers/session-initialization-prune.js +50 -0
- package/dist/modules/payment_gateways/workers/session-initialization-prune.js.map +7 -0
- package/dist/modules/portal/frontend/[orgSlug]/portal/invite/page.js +134 -0
- package/dist/modules/portal/frontend/[orgSlug]/portal/invite/page.js.map +7 -0
- package/dist/modules/portal/frontend/[orgSlug]/portal/invite/page.meta.js +11 -0
- package/dist/modules/portal/frontend/[orgSlug]/portal/invite/page.meta.js.map +7 -0
- package/dist/modules/portal/lib/navigation.js +7 -0
- package/dist/modules/portal/lib/navigation.js.map +7 -0
- package/dist/modules/query_index/lib/engine.js +43 -0
- package/dist/modules/query_index/lib/engine.js.map +2 -2
- package/dist/modules/query_index/lib/search-tokens.js +15 -5
- package/dist/modules/query_index/lib/search-tokens.js.map +2 -2
- package/dist/modules/shipping_carriers/api/webhook/[provider]/route.js +11 -1
- package/dist/modules/shipping_carriers/api/webhook/[provider]/route.js.map +2 -2
- package/generated/entities/customer_deal_person_link/index.ts +1 -0
- package/generated/entities/customer_user_invitation/index.ts +1 -0
- package/generated/entity-fields-registry.ts +2 -0
- package/package.json +7 -7
- package/src/modules/business_rules/api/openmercato-call-options/route.ts +94 -0
- package/src/modules/business_rules/api/rules/route.ts +92 -4
- package/src/modules/business_rules/components/ActionBuilder.tsx +54 -5
- package/src/modules/business_rules/components/ActionRow.tsx +156 -18
- package/src/modules/business_rules/components/utils/actionValidation.ts +6 -0
- package/src/modules/business_rules/i18n/de.json +12 -0
- package/src/modules/business_rules/i18n/en.json +12 -0
- package/src/modules/business_rules/i18n/es.json +12 -0
- package/src/modules/business_rules/i18n/pl.json +12 -0
- package/src/modules/business_rules/lib/action-executor.ts +198 -0
- package/src/modules/business_rules/lib/openmercato-call-options-types.ts +31 -0
- package/src/modules/business_rules/lib/openmercato-call-options.ts +297 -0
- package/src/modules/business_rules/lib/rule-engine.ts +4 -0
- package/src/modules/communication_channels/api/post/webhook/[provider]/route.ts +11 -1
- package/src/modules/communication_channels/api/post/webhooks/gmail/route.ts +17 -11
- package/src/modules/configs/lib/upgrade-actions.ts +14 -0
- package/src/modules/customer_accounts/api/admin/users/[id].ts +16 -1
- package/src/modules/customer_accounts/api/admin/users-invite.ts +78 -3
- package/src/modules/customer_accounts/api/portal/users-invite.ts +22 -1
- package/src/modules/customer_accounts/data/entities.ts +3 -0
- package/src/modules/customer_accounts/data/validators.ts +1 -0
- package/src/modules/customer_accounts/emails/CustomerInvitationEmail.tsx +45 -0
- package/src/modules/customer_accounts/i18n/de.json +6 -0
- package/src/modules/customer_accounts/i18n/en.json +6 -0
- package/src/modules/customer_accounts/i18n/es.json +6 -0
- package/src/modules/customer_accounts/i18n/pl.json +6 -0
- package/src/modules/customer_accounts/lib/customerEntityOwnership.ts +68 -2
- package/src/modules/customer_accounts/lib/customerUrl.ts +27 -11
- package/src/modules/customer_accounts/lib/invitationEmail.ts +42 -0
- package/src/modules/customer_accounts/migrations/.snapshot-open-mercato.json +16 -0
- package/src/modules/customer_accounts/migrations/Migration20260723120000_customer_accounts.ts +13 -0
- package/src/modules/customer_accounts/services/customerInvitationService.ts +56 -3
- package/src/modules/customer_accounts/subscribers/autoLinkCrm.ts +75 -8
- package/src/modules/customer_accounts/widgets/injection/account-status/widget.client.tsx +2 -2
- package/src/modules/customer_accounts/widgets/injection-table.ts +4 -1
- package/src/modules/customers/api/deals/[id]/people/route.ts +3 -0
- package/src/modules/customers/api/deals/[id]/route.ts +10 -2
- package/src/modules/customers/backend/customers/companies-v2/[id]/page.tsx +12 -1
- package/src/modules/customers/backend/customers/deals/[id]/hooks/types.ts +1 -0
- package/src/modules/customers/backend/customers/deals/[id]/page.tsx +15 -9
- package/src/modules/customers/backend/customers/people-v2/[id]/page.tsx +24 -6
- package/src/modules/customers/commands/deals.ts +50 -9
- package/src/modules/customers/commands/people.ts +3 -0
- package/src/modules/customers/components/detail/CompanyDetailTabs.tsx +30 -7
- package/src/modules/customers/components/detail/DealDetailTabs.tsx +34 -9
- package/src/modules/customers/components/detail/PersonDetailTabs.tsx +34 -9
- package/src/modules/customers/data/entities.ts +9 -1
- package/src/modules/customers/data/validators.ts +1 -0
- package/src/modules/customers/i18n/de.json +1 -0
- package/src/modules/customers/i18n/en.json +1 -0
- package/src/modules/customers/i18n/es.json +1 -0
- package/src/modules/customers/i18n/pl.json +1 -0
- package/src/modules/customers/migrations/.snapshot-open-mercato.json +27 -1
- package/src/modules/customers/migrations/Migration20260723210000_customers.ts +15 -0
- package/src/modules/entities/api/entity-settings.ts +66 -22
- package/src/modules/entities/backend/config/settings/page.meta.ts +34 -0
- package/src/modules/entities/backend/config/settings/page.tsx +12 -0
- package/src/modules/entities/backend/entities/user/create/page.tsx +16 -7
- package/src/modules/entities/components/EntitySettingsManager.tsx +160 -0
- package/src/modules/entities/i18n/de.json +9 -0
- package/src/modules/entities/i18n/en.json +9 -0
- package/src/modules/entities/i18n/es.json +9 -0
- package/src/modules/entities/i18n/pl.json +9 -0
- package/src/modules/inbox_ops/api/webhook/inbound.ts +14 -12
- package/src/modules/payment_gateways/api/webhook/[provider]/route.ts +13 -1
- package/src/modules/payment_gateways/data/entities.ts +5 -0
- package/src/modules/payment_gateways/i18n/de.json +5 -1
- package/src/modules/payment_gateways/i18n/en.json +5 -1
- package/src/modules/payment_gateways/i18n/es.json +5 -1
- package/src/modules/payment_gateways/i18n/pl.json +5 -1
- package/src/modules/payment_gateways/lib/gateway-service.ts +12 -2
- package/src/modules/payment_gateways/lib/session-idempotency.ts +36 -0
- package/src/modules/payment_gateways/migrations/.snapshot-open-mercato.json +10 -1
- package/src/modules/payment_gateways/migrations/Migration20260803103035_payment_gateways.ts +17 -0
- package/src/modules/payment_gateways/setup.ts +62 -0
- package/src/modules/payment_gateways/workers/session-initialization-prune.ts +63 -0
- package/src/modules/portal/frontend/[orgSlug]/portal/invite/page.meta.ts +9 -0
- package/src/modules/portal/frontend/[orgSlug]/portal/invite/page.tsx +164 -0
- package/src/modules/portal/i18n/de.json +18 -0
- package/src/modules/portal/i18n/en.json +18 -0
- package/src/modules/portal/i18n/es.json +18 -0
- package/src/modules/portal/i18n/pl.json +18 -0
- package/src/modules/portal/lib/navigation.ts +3 -0
- package/src/modules/query_index/lib/engine.ts +52 -0
- package/src/modules/query_index/lib/search-tokens.ts +16 -4
- package/src/modules/shipping_carriers/api/webhook/[provider]/route.ts +11 -1
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
2
|
+
import { createModuleQueue, type JobContext, type Queue, type QueuedJob, type WorkerMeta } from '@open-mercato/queue'
|
|
3
|
+
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
4
|
+
import {
|
|
5
|
+
PAYMENT_SESSION_INITIALIZATION_PRUNE_QUEUE,
|
|
6
|
+
PAYMENT_SESSION_INITIALIZATION_PRUNE_BATCH_SIZE,
|
|
7
|
+
pruneCompletedPaymentSessionInitializations,
|
|
8
|
+
} from '../lib/session-idempotency'
|
|
9
|
+
|
|
10
|
+
type PrunePayload = {
|
|
11
|
+
tenantId?: string
|
|
12
|
+
organizationId?: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
type HandlerContext = JobContext & {
|
|
16
|
+
resolve: <T = unknown>(name: string) => T
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const logger = createLogger('payment_gateways')
|
|
20
|
+
let continuationQueue: Queue<PrunePayload> | null = null
|
|
21
|
+
|
|
22
|
+
function getContinuationQueue(): Queue<PrunePayload> {
|
|
23
|
+
continuationQueue ??= createModuleQueue<PrunePayload>(
|
|
24
|
+
PAYMENT_SESSION_INITIALIZATION_PRUNE_QUEUE,
|
|
25
|
+
{ concurrency: 1 },
|
|
26
|
+
)
|
|
27
|
+
return continuationQueue
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const metadata: WorkerMeta = {
|
|
31
|
+
queue: PAYMENT_SESSION_INITIALIZATION_PRUNE_QUEUE,
|
|
32
|
+
id: 'payment_gateways:session-initialization-prune',
|
|
33
|
+
concurrency: 1,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export default async function handle(
|
|
37
|
+
job: QueuedJob<PrunePayload>,
|
|
38
|
+
ctx: HandlerContext,
|
|
39
|
+
): Promise<void> {
|
|
40
|
+
const tenantId = job.payload?.tenantId
|
|
41
|
+
const organizationId = job.payload?.organizationId
|
|
42
|
+
if (!tenantId || !organizationId) {
|
|
43
|
+
logger.warn('Skipping payment-session initialization prune without tenant scope')
|
|
44
|
+
return
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const em = ctx.resolve<EntityManager>('em').fork()
|
|
48
|
+
const deleted = await pruneCompletedPaymentSessionInitializations(
|
|
49
|
+
em,
|
|
50
|
+
{ tenantId, organizationId },
|
|
51
|
+
{ batchSize: PAYMENT_SESSION_INITIALIZATION_PRUNE_BATCH_SIZE },
|
|
52
|
+
)
|
|
53
|
+
if (deleted > 0) {
|
|
54
|
+
logger.info('Pruned completed payment-session initialization claims', {
|
|
55
|
+
deleted,
|
|
56
|
+
tenantId,
|
|
57
|
+
organizationId,
|
|
58
|
+
})
|
|
59
|
+
}
|
|
60
|
+
if (deleted >= PAYMENT_SESSION_INITIALIZATION_PRUNE_BATCH_SIZE) {
|
|
61
|
+
await getContinuationQueue().enqueue({ tenantId, organizationId }, { delayMs: 1_000 })
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
import { useCallback, useMemo, useState, useEffect } from 'react'
|
|
3
|
+
import { useSearchParams } from 'next/navigation'
|
|
4
|
+
import Link from 'next/link'
|
|
5
|
+
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
6
|
+
import { PasswordInput } from '@open-mercato/ui/primitives/password-input'
|
|
7
|
+
import { Input } from '@open-mercato/ui/primitives/input'
|
|
8
|
+
import { Label } from '@open-mercato/ui/primitives/label'
|
|
9
|
+
import { Button } from '@open-mercato/ui/primitives/button'
|
|
10
|
+
import { Alert, AlertDescription } from '@open-mercato/ui/primitives/alert'
|
|
11
|
+
import { EmptyState } from '@open-mercato/ui/primitives/empty-state'
|
|
12
|
+
import { SearchX } from 'lucide-react'
|
|
13
|
+
import { Spinner } from '@open-mercato/ui/primitives/spinner'
|
|
14
|
+
import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
|
|
15
|
+
import { usePortalContext } from '@open-mercato/ui/portal/PortalContext'
|
|
16
|
+
import { InjectionSpot } from '@open-mercato/ui/backend/injection/InjectionSpot'
|
|
17
|
+
import { PortalInjectionSpots } from '@open-mercato/ui/backend/injection/spotIds'
|
|
18
|
+
import { navigateWithPageReload } from '@open-mercato/core/modules/portal/lib/navigation'
|
|
19
|
+
|
|
20
|
+
type Props = { params: { orgSlug: string } }
|
|
21
|
+
|
|
22
|
+
export default function PortalInvitePage({ params }: Props) {
|
|
23
|
+
const t = useT()
|
|
24
|
+
const orgSlug = params.orgSlug
|
|
25
|
+
const { tenant } = usePortalContext()
|
|
26
|
+
const searchParams = useSearchParams()
|
|
27
|
+
|
|
28
|
+
const [displayName, setDisplayName] = useState('')
|
|
29
|
+
const [password, setPassword] = useState('')
|
|
30
|
+
const [confirmPassword, setConfirmPassword] = useState('')
|
|
31
|
+
const [error, setError] = useState<string | null>(null)
|
|
32
|
+
const [submitting, setSubmitting] = useState(false)
|
|
33
|
+
const [token, setToken] = useState<string | null>(null)
|
|
34
|
+
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
const tokenParam = searchParams.get('token')
|
|
37
|
+
if (!tokenParam) {
|
|
38
|
+
setError(t('portal.invite.error.noToken', 'Invalid or missing invitation token.'))
|
|
39
|
+
} else {
|
|
40
|
+
setToken(tokenParam)
|
|
41
|
+
}
|
|
42
|
+
}, [searchParams, t])
|
|
43
|
+
|
|
44
|
+
const handleSubmit = useCallback(
|
|
45
|
+
async (event: React.FormEvent) => {
|
|
46
|
+
event.preventDefault()
|
|
47
|
+
setError(null)
|
|
48
|
+
|
|
49
|
+
if (!token) {
|
|
50
|
+
setError(t('portal.invite.error.noToken', 'Invalid or missing invitation token.'))
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (!displayName.trim()) {
|
|
55
|
+
setError(t('portal.invite.error.displayNameRequired', 'Display name is required.'))
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (password !== confirmPassword) {
|
|
60
|
+
setError(t('portal.invite.error.passwordMismatch', 'Passwords do not match.'))
|
|
61
|
+
return
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (password.length < 8) {
|
|
65
|
+
setError(t('portal.invite.error.passwordTooShort', 'Password must be at least 8 characters long.'))
|
|
66
|
+
return
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
setSubmitting(true)
|
|
70
|
+
try {
|
|
71
|
+
const result = await apiCall<{ ok: boolean; error?: string }>('/api/customer_accounts/invitations/accept', {
|
|
72
|
+
method: 'POST',
|
|
73
|
+
headers: { 'Content-Type': 'application/json' },
|
|
74
|
+
body: JSON.stringify({ token, password, displayName: displayName.trim() }),
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
if (result.ok && result.result?.ok) {
|
|
78
|
+
navigateWithPageReload(`/${orgSlug}/portal/dashboard`)
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (result.status === 400) {
|
|
83
|
+
setError(t('portal.invite.error.invalidToken', 'Invalid or expired invitation.'))
|
|
84
|
+
} else {
|
|
85
|
+
setError(result.result?.error || t('portal.invite.error.generic', 'Invitation acceptance failed. Please try again.'))
|
|
86
|
+
}
|
|
87
|
+
} catch {
|
|
88
|
+
setError(t('portal.invite.error.generic', 'Invitation acceptance failed. Please try again.'))
|
|
89
|
+
} finally {
|
|
90
|
+
setSubmitting(false)
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
[token, password, confirmPassword, displayName, orgSlug, t],
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
const injectionContext = useMemo(
|
|
97
|
+
() => ({ orgSlug }),
|
|
98
|
+
[orgSlug],
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
if (tenant.loading) {
|
|
102
|
+
return <div className="flex items-center justify-center py-20"><Spinner /></div>
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (tenant.error) {
|
|
106
|
+
return (
|
|
107
|
+
<div className="mx-auto w-full max-w-md py-12">
|
|
108
|
+
<EmptyState
|
|
109
|
+
variant="subtle"
|
|
110
|
+
size="lg"
|
|
111
|
+
icon={<SearchX className="h-6 w-6" aria-hidden />}
|
|
112
|
+
title={t('portal.org.invalid', 'Organization not found.')}
|
|
113
|
+
/>
|
|
114
|
+
</div>
|
|
115
|
+
)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return (
|
|
119
|
+
<div className="mx-auto w-full max-w-sm">
|
|
120
|
+
<div className="mb-8 text-center">
|
|
121
|
+
<h1 className="text-2xl font-bold tracking-tight">{t('portal.invite.title', 'Accept Invitation')}</h1>
|
|
122
|
+
<p className="mt-1.5 text-sm text-muted-foreground">{t('portal.invite.description', 'Create your portal account to accept this invitation.')}</p>
|
|
123
|
+
</div>
|
|
124
|
+
|
|
125
|
+
<InjectionSpot spotId={PortalInjectionSpots.pageBefore('invite')} context={injectionContext} />
|
|
126
|
+
|
|
127
|
+
<form onSubmit={handleSubmit} className="flex flex-col gap-4">
|
|
128
|
+
{error ? (
|
|
129
|
+
<Alert variant="destructive">
|
|
130
|
+
<AlertDescription>{error}</AlertDescription>
|
|
131
|
+
</Alert>
|
|
132
|
+
) : null}
|
|
133
|
+
|
|
134
|
+
<div className="flex flex-col gap-1.5">
|
|
135
|
+
<Label htmlFor="invite-display-name" className="text-overline font-semibold uppercase tracking-wider text-muted-foreground/70">{t('portal.invite.displayName', 'Display Name')}</Label>
|
|
136
|
+
<Input id="invite-display-name" autoComplete="name" required value={displayName} onChange={(e) => setDisplayName(e.target.value)} disabled={submitting || !token} className="rounded-lg" />
|
|
137
|
+
</div>
|
|
138
|
+
|
|
139
|
+
<div className="flex flex-col gap-1.5">
|
|
140
|
+
<Label htmlFor="invite-password" className="text-overline font-semibold uppercase tracking-wider text-muted-foreground/70">{t('portal.invite.password', 'Password')}</Label>
|
|
141
|
+
<PasswordInput id="invite-password" autoComplete="new-password" required placeholder={t('portal.invite.password.placeholder', '••••••••')} value={password} onChange={(e) => setPassword(e.target.value)} disabled={submitting || !token} className="rounded-lg" />
|
|
142
|
+
</div>
|
|
143
|
+
|
|
144
|
+
<div className="flex flex-col gap-1.5">
|
|
145
|
+
<Label htmlFor="invite-confirm-password" className="text-overline font-semibold uppercase tracking-wider text-muted-foreground/70">{t('portal.invite.confirmPassword', 'Confirm Password')}</Label>
|
|
146
|
+
<PasswordInput id="invite-confirm-password" autoComplete="new-password" required placeholder={t('portal.invite.confirmPassword.placeholder', '••••••••')} value={confirmPassword} onChange={(e) => setConfirmPassword(e.target.value)} disabled={submitting || !token} className="rounded-lg" />
|
|
147
|
+
</div>
|
|
148
|
+
|
|
149
|
+
<Button type="submit" disabled={submitting || !token} className="mt-1 w-full rounded-lg">
|
|
150
|
+
{submitting ? t('portal.invite.submitting', 'Accepting invitation...') : t('portal.invite.submit', 'Accept Invitation')}
|
|
151
|
+
</Button>
|
|
152
|
+
|
|
153
|
+
<p className="text-center text-sm text-muted-foreground">
|
|
154
|
+
{t('portal.invite.backToLogin', 'Already have an account?')}{' '}
|
|
155
|
+
<Link href={`/${orgSlug}/portal/login`} className="font-medium text-foreground underline underline-offset-4 hover:opacity-80">
|
|
156
|
+
{t('portal.invite.loginLink', 'Sign in')}
|
|
157
|
+
</Link>
|
|
158
|
+
</p>
|
|
159
|
+
</form>
|
|
160
|
+
|
|
161
|
+
<InjectionSpot spotId={PortalInjectionSpots.pageAfter('invite')} context={injectionContext} />
|
|
162
|
+
</div>
|
|
163
|
+
)
|
|
164
|
+
}
|
|
@@ -22,6 +22,23 @@
|
|
|
22
22
|
"portal.disabled.title": "Portal nicht verfügbar",
|
|
23
23
|
"portal.footer.copyright": "© {year} Alle Rechte vorbehalten.",
|
|
24
24
|
"portal.info.instructions": "Um auf das Portal Ihrer Organisation zuzugreifen, navigieren Sie zur eindeutigen URL Ihrer Organisation. Kontaktieren Sie Ihren Administrator, wenn Sie den Portal-Link nicht kennen.",
|
|
25
|
+
"portal.invite.backToLogin": "Already have an account?",
|
|
26
|
+
"portal.invite.confirmPassword": "Confirm Password",
|
|
27
|
+
"portal.invite.confirmPassword.placeholder": "••••••••",
|
|
28
|
+
"portal.invite.description": "Create your portal account to accept this invitation.",
|
|
29
|
+
"portal.invite.displayName": "Display Name",
|
|
30
|
+
"portal.invite.error.displayNameRequired": "Display name is required.",
|
|
31
|
+
"portal.invite.error.generic": "Invitation acceptance failed. Please try again.",
|
|
32
|
+
"portal.invite.error.invalidToken": "Invalid or expired invitation.",
|
|
33
|
+
"portal.invite.error.noToken": "Invalid or missing invitation token.",
|
|
34
|
+
"portal.invite.error.passwordMismatch": "Passwords do not match.",
|
|
35
|
+
"portal.invite.error.passwordTooShort": "Password must be at least 8 characters long.",
|
|
36
|
+
"portal.invite.loginLink": "Sign in",
|
|
37
|
+
"portal.invite.password": "Password",
|
|
38
|
+
"portal.invite.password.placeholder": "••••••••",
|
|
39
|
+
"portal.invite.submit": "Accept Invitation",
|
|
40
|
+
"portal.invite.submitting": "Accepting invitation…",
|
|
41
|
+
"portal.invite.title": "Accept Invitation",
|
|
25
42
|
"portal.landing.cta.login": "Anmelden",
|
|
26
43
|
"portal.landing.cta.signup": "Konto erstellen",
|
|
27
44
|
"portal.landing.feature.account": "Account Management",
|
|
@@ -49,6 +66,7 @@
|
|
|
49
66
|
"portal.nav.account": "Konto",
|
|
50
67
|
"portal.nav.dashboard": "Dashboard",
|
|
51
68
|
"portal.nav.home": "Portal",
|
|
69
|
+
"portal.nav.invite": "Accept Invitation",
|
|
52
70
|
"portal.nav.login": "Anmelden",
|
|
53
71
|
"portal.nav.logout": "Abmelden",
|
|
54
72
|
"portal.nav.profile": "Profil",
|
|
@@ -22,6 +22,23 @@
|
|
|
22
22
|
"portal.disabled.title": "Portal Not Available",
|
|
23
23
|
"portal.footer.copyright": "© {year} All rights reserved.",
|
|
24
24
|
"portal.info.instructions": "To access your organization's portal, navigate to your organization's unique URL. Contact your administrator if you don't know your portal link.",
|
|
25
|
+
"portal.invite.backToLogin": "Already have an account?",
|
|
26
|
+
"portal.invite.confirmPassword": "Confirm Password",
|
|
27
|
+
"portal.invite.confirmPassword.placeholder": "••••••••",
|
|
28
|
+
"portal.invite.description": "Create your portal account to accept this invitation.",
|
|
29
|
+
"portal.invite.displayName": "Display Name",
|
|
30
|
+
"portal.invite.error.displayNameRequired": "Display name is required.",
|
|
31
|
+
"portal.invite.error.generic": "Invitation acceptance failed. Please try again.",
|
|
32
|
+
"portal.invite.error.invalidToken": "Invalid or expired invitation.",
|
|
33
|
+
"portal.invite.error.noToken": "Invalid or missing invitation token.",
|
|
34
|
+
"portal.invite.error.passwordMismatch": "Passwords do not match.",
|
|
35
|
+
"portal.invite.error.passwordTooShort": "Password must be at least 8 characters long.",
|
|
36
|
+
"portal.invite.loginLink": "Sign in",
|
|
37
|
+
"portal.invite.password": "Password",
|
|
38
|
+
"portal.invite.password.placeholder": "••••••••",
|
|
39
|
+
"portal.invite.submit": "Accept Invitation",
|
|
40
|
+
"portal.invite.submitting": "Accepting invitation…",
|
|
41
|
+
"portal.invite.title": "Accept Invitation",
|
|
25
42
|
"portal.landing.cta.login": "Log In",
|
|
26
43
|
"portal.landing.cta.signup": "Create Account",
|
|
27
44
|
"portal.landing.feature.account": "Account Management",
|
|
@@ -49,6 +66,7 @@
|
|
|
49
66
|
"portal.nav.account": "Account",
|
|
50
67
|
"portal.nav.dashboard": "Dashboard",
|
|
51
68
|
"portal.nav.home": "Portal",
|
|
69
|
+
"portal.nav.invite": "Accept Invitation",
|
|
52
70
|
"portal.nav.login": "Log In",
|
|
53
71
|
"portal.nav.logout": "Log Out",
|
|
54
72
|
"portal.nav.profile": "Profile",
|
|
@@ -22,6 +22,23 @@
|
|
|
22
22
|
"portal.disabled.title": "Portal no disponible",
|
|
23
23
|
"portal.footer.copyright": "© {year} Todos los derechos reservados.",
|
|
24
24
|
"portal.info.instructions": "Para acceder al portal de su organización, navegue a la URL única de su organización. Contacte a su administrador si no conoce el enlace del portal.",
|
|
25
|
+
"portal.invite.backToLogin": "Already have an account?",
|
|
26
|
+
"portal.invite.confirmPassword": "Confirm Password",
|
|
27
|
+
"portal.invite.confirmPassword.placeholder": "••••••••",
|
|
28
|
+
"portal.invite.description": "Create your portal account to accept this invitation.",
|
|
29
|
+
"portal.invite.displayName": "Display Name",
|
|
30
|
+
"portal.invite.error.displayNameRequired": "Display name is required.",
|
|
31
|
+
"portal.invite.error.generic": "Invitation acceptance failed. Please try again.",
|
|
32
|
+
"portal.invite.error.invalidToken": "Invalid or expired invitation.",
|
|
33
|
+
"portal.invite.error.noToken": "Invalid or missing invitation token.",
|
|
34
|
+
"portal.invite.error.passwordMismatch": "Passwords do not match.",
|
|
35
|
+
"portal.invite.error.passwordTooShort": "Password must be at least 8 characters long.",
|
|
36
|
+
"portal.invite.loginLink": "Sign in",
|
|
37
|
+
"portal.invite.password": "Password",
|
|
38
|
+
"portal.invite.password.placeholder": "••••••••",
|
|
39
|
+
"portal.invite.submit": "Accept Invitation",
|
|
40
|
+
"portal.invite.submitting": "Accepting invitation…",
|
|
41
|
+
"portal.invite.title": "Accept Invitation",
|
|
25
42
|
"portal.landing.cta.login": "Iniciar sesión",
|
|
26
43
|
"portal.landing.cta.signup": "Crear cuenta",
|
|
27
44
|
"portal.landing.feature.account": "Account Management",
|
|
@@ -49,6 +66,7 @@
|
|
|
49
66
|
"portal.nav.account": "Cuenta",
|
|
50
67
|
"portal.nav.dashboard": "Panel",
|
|
51
68
|
"portal.nav.home": "Portal",
|
|
69
|
+
"portal.nav.invite": "Accept Invitation",
|
|
52
70
|
"portal.nav.login": "Iniciar sesión",
|
|
53
71
|
"portal.nav.logout": "Cerrar sesión",
|
|
54
72
|
"portal.nav.profile": "Perfil",
|
|
@@ -22,6 +22,23 @@
|
|
|
22
22
|
"portal.disabled.title": "Portal niedostępny",
|
|
23
23
|
"portal.footer.copyright": "© {year} Wszelkie prawa zastrzeżone.",
|
|
24
24
|
"portal.info.instructions": "Aby uzyskać dostęp do portalu organizacji, przejdź do unikalnego adresu URL swojej organizacji. Skontaktuj się z administratorem, jeśli nie znasz linku do portalu.",
|
|
25
|
+
"portal.invite.backToLogin": "Masz już konto?",
|
|
26
|
+
"portal.invite.confirmPassword": "Potwierdź hasło",
|
|
27
|
+
"portal.invite.confirmPassword.placeholder": "••••••••",
|
|
28
|
+
"portal.invite.description": "Utwórz konto w portalu, aby przyjąć to zaproszenie.",
|
|
29
|
+
"portal.invite.displayName": "Nazwa wyświetlana",
|
|
30
|
+
"portal.invite.error.displayNameRequired": "Nazwa wyświetlana jest wymagana.",
|
|
31
|
+
"portal.invite.error.generic": "Nie udało się przyjąć zaproszenia. Spróbuj ponownie.",
|
|
32
|
+
"portal.invite.error.invalidToken": "Nieprawidłowe lub wygasłe zaproszenie.",
|
|
33
|
+
"portal.invite.error.noToken": "Nieprawidłowy lub brakujący token zaproszenia.",
|
|
34
|
+
"portal.invite.error.passwordMismatch": "Hasła nie pasują do siebie.",
|
|
35
|
+
"portal.invite.error.passwordTooShort": "Hasło musi mieć co najmniej 8 znaków.",
|
|
36
|
+
"portal.invite.loginLink": "Zaloguj się",
|
|
37
|
+
"portal.invite.password": "Hasło",
|
|
38
|
+
"portal.invite.password.placeholder": "••••••••",
|
|
39
|
+
"portal.invite.submit": "Przyjmij zaproszenie",
|
|
40
|
+
"portal.invite.submitting": "Przyjmowanie zaproszenia…",
|
|
41
|
+
"portal.invite.title": "Przyjmij zaproszenie",
|
|
25
42
|
"portal.landing.cta.login": "Zaloguj się",
|
|
26
43
|
"portal.landing.cta.signup": "Utwórz konto",
|
|
27
44
|
"portal.landing.feature.account": "Account Management",
|
|
@@ -49,6 +66,7 @@
|
|
|
49
66
|
"portal.nav.account": "Konto",
|
|
50
67
|
"portal.nav.dashboard": "Panel",
|
|
51
68
|
"portal.nav.home": "Portal",
|
|
69
|
+
"portal.nav.invite": "Przyjmij zaproszenie",
|
|
52
70
|
"portal.nav.login": "Zaloguj się",
|
|
53
71
|
"portal.nav.logout": "Wyloguj się",
|
|
54
72
|
"portal.nav.profile": "Profil",
|
|
@@ -34,10 +34,46 @@ import { warnOnCiphertextLikeFallback } from '@open-mercato/shared/lib/query/cip
|
|
|
34
34
|
import { resolveEncryptedSortFields, resolveEncryptedSortMaxRows, sortRowsInMemory } from '@open-mercato/shared/lib/query/encrypted-sort'
|
|
35
35
|
import { mapWithConcurrency } from '@open-mercato/shared/lib/query/bounded-decrypt'
|
|
36
36
|
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
37
|
+
import { parseNumberWithDefault } from '@open-mercato/shared/lib/number'
|
|
37
38
|
|
|
38
39
|
const logger = createLogger('query_index').child({ component: 'engine' })
|
|
39
40
|
|
|
40
41
|
const DECRYPT_CONCURRENCY = 8
|
|
42
|
+
const AUTO_REINDEX_DEBOUNCE_DEFAULT_MS = 30_000
|
|
43
|
+
const AUTO_REINDEX_DEBOUNCE_MAX_SCOPES = 10_000
|
|
44
|
+
const AUTO_REINDEX_SCHEDULED_AT_KEY = Symbol.for('@open-mercato/query-index/auto-reindex-scheduled-at')
|
|
45
|
+
|
|
46
|
+
type GlobalWithAutoReindexSchedule = typeof globalThis & {
|
|
47
|
+
[AUTO_REINDEX_SCHEDULED_AT_KEY]?: Map<string, number>
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function getAutoReindexScheduledAt(): Map<string, number> {
|
|
51
|
+
const globalScope = globalThis as GlobalWithAutoReindexSchedule
|
|
52
|
+
if (!globalScope[AUTO_REINDEX_SCHEDULED_AT_KEY]) {
|
|
53
|
+
globalScope[AUTO_REINDEX_SCHEDULED_AT_KEY] = new Map<string, number>()
|
|
54
|
+
}
|
|
55
|
+
return globalScope[AUTO_REINDEX_SCHEDULED_AT_KEY]
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function markAutoReindexScheduled(key: string, debounceMs: number, now: number): boolean {
|
|
59
|
+
const autoReindexScheduledAt = getAutoReindexScheduledAt()
|
|
60
|
+
const previous = autoReindexScheduledAt.get(key)
|
|
61
|
+
if (previous !== undefined && now - previous < debounceMs) return false
|
|
62
|
+
|
|
63
|
+
if (autoReindexScheduledAt.size >= AUTO_REINDEX_DEBOUNCE_MAX_SCOPES) {
|
|
64
|
+
for (const [scopeKey, scheduledAt] of autoReindexScheduledAt) {
|
|
65
|
+
if (now - scheduledAt >= debounceMs) autoReindexScheduledAt.delete(scopeKey)
|
|
66
|
+
}
|
|
67
|
+
if (autoReindexScheduledAt.size >= AUTO_REINDEX_DEBOUNCE_MAX_SCOPES) {
|
|
68
|
+
const oldestKey = autoReindexScheduledAt.keys().next().value
|
|
69
|
+
if (oldestKey !== undefined) autoReindexScheduledAt.delete(oldestKey)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
autoReindexScheduledAt.delete(key)
|
|
74
|
+
autoReindexScheduledAt.set(key, now)
|
|
75
|
+
return true
|
|
76
|
+
}
|
|
41
77
|
|
|
42
78
|
function buildFilterableCustomFieldJoins(
|
|
43
79
|
sources: QueryCustomFieldSource[] | undefined,
|
|
@@ -152,6 +188,7 @@ export class HybridQueryEngine implements QueryEngine {
|
|
|
152
188
|
private sqlDebugEnabled: boolean | null = null
|
|
153
189
|
private forcePartialIndexEnabled: boolean | null = null
|
|
154
190
|
private autoReindexEnabled: boolean | null = null
|
|
191
|
+
private autoReindexDebounceMs: number | null = null
|
|
155
192
|
private coverageOptimizationEnabled: boolean | null = null
|
|
156
193
|
private pendingCoverageRefreshKeys = new Set<string>()
|
|
157
194
|
private searchAvailabilityInstance: SearchTokenAvailability | null = null
|
|
@@ -1998,6 +2035,11 @@ export class HybridQueryEngine implements QueryEngine {
|
|
|
1998
2035
|
organizationId: organizationIdOverride ?? opts.organizationId ?? null,
|
|
1999
2036
|
force: false,
|
|
2000
2037
|
}
|
|
2038
|
+
const debounceMs = this.getAutoReindexDebounceMs()
|
|
2039
|
+
if (debounceMs > 0) {
|
|
2040
|
+
const key = [payload.entityType, payload.tenantId ?? '__tenant__', payload.organizationId ?? '__org__'].join('|')
|
|
2041
|
+
if (!markAutoReindexScheduled(key, debounceMs, Date.now())) return
|
|
2042
|
+
}
|
|
2001
2043
|
const context = stats
|
|
2002
2044
|
? { entity, tenantId: payload.tenantId, organizationId: payload.organizationId, baseCount: stats.baseCount, indexedCount: stats.indexedCount }
|
|
2003
2045
|
: { entity, tenantId: payload.tenantId, organizationId: payload.organizationId }
|
|
@@ -2067,6 +2109,16 @@ export class HybridQueryEngine implements QueryEngine {
|
|
|
2067
2109
|
return this.autoReindexEnabled
|
|
2068
2110
|
}
|
|
2069
2111
|
|
|
2112
|
+
private getAutoReindexDebounceMs(): number {
|
|
2113
|
+
if (this.autoReindexDebounceMs != null) return this.autoReindexDebounceMs
|
|
2114
|
+
this.autoReindexDebounceMs = parseNumberWithDefault(
|
|
2115
|
+
process.env.OM_QUERY_INDEX_AUTO_REINDEX_DEBOUNCE_MS,
|
|
2116
|
+
AUTO_REINDEX_DEBOUNCE_DEFAULT_MS,
|
|
2117
|
+
{ integer: true, min: 0 },
|
|
2118
|
+
)
|
|
2119
|
+
return this.autoReindexDebounceMs
|
|
2120
|
+
}
|
|
2121
|
+
|
|
2070
2122
|
private isCoverageOptimizationEnabled(): boolean {
|
|
2071
2123
|
if (this.coverageOptimizationEnabled != null) return this.coverageOptimizationEnabled
|
|
2072
2124
|
const raw = (process.env.OPTIMIZE_INDEX_COVERAGE_STATS ?? '').trim().toLowerCase()
|
|
@@ -2,6 +2,7 @@ import { type Kysely, type Transaction, sql } from 'kysely'
|
|
|
2
2
|
import {
|
|
3
3
|
isSearchFieldBlocklisted,
|
|
4
4
|
resolveSearchConfig,
|
|
5
|
+
resolveSearchTokenLimits,
|
|
5
6
|
type SearchConfig,
|
|
6
7
|
} from '@open-mercato/shared/lib/search/config'
|
|
7
8
|
import { tokenizeText } from '@open-mercato/shared/lib/search/tokenize'
|
|
@@ -79,9 +80,7 @@ function shouldIndexField(
|
|
|
79
80
|
if (lower.endsWith('_at')) return false
|
|
80
81
|
if (['created_at', 'updated_at', 'deleted_at', 'tenant_id', 'organization_id'].includes(lower)) return false
|
|
81
82
|
if (isSearchFieldBlocklisted(field, entityType, config)) return false
|
|
82
|
-
|
|
83
|
-
if (!values.length) return false
|
|
84
|
-
return values.some((text) => tokenizeText(text, config).tokens.length > 0)
|
|
83
|
+
return collectTextValues(value).some((text) => text.length > 0)
|
|
85
84
|
}
|
|
86
85
|
|
|
87
86
|
export function buildSearchTokenRows(params: BuildTokenOptions): SearchTokenRow[] {
|
|
@@ -95,19 +94,32 @@ export function buildSearchTokenRows(params: BuildTokenOptions): SearchTokenRow[
|
|
|
95
94
|
organizationId: params.organizationId ?? DEFAULT_SCOPE.organizationId,
|
|
96
95
|
tenantId: params.tenantId ?? DEFAULT_SCOPE.tenantId,
|
|
97
96
|
}
|
|
97
|
+
const limits = resolveSearchTokenLimits(config)
|
|
98
|
+
const recordLimit = limits.maxTokensPerRecord > 0 ? limits.maxTokensPerRecord : Number.POSITIVE_INFINITY
|
|
99
|
+
const fieldLimit = limits.maxTokensPerField > 0 ? limits.maxTokensPerField : Number.POSITIVE_INFINITY
|
|
98
100
|
|
|
99
101
|
for (const [field, rawValue] of Object.entries(params.doc)) {
|
|
102
|
+
if (tokens.length >= recordLimit) break
|
|
100
103
|
if (!shouldIndexField(field, rawValue, config, params.entityType)) continue
|
|
101
104
|
const values = collectTextValues(rawValue)
|
|
102
105
|
const seen = new Set<string>()
|
|
106
|
+
let fieldTokenCount = 0
|
|
103
107
|
for (const text of values) {
|
|
104
|
-
|
|
108
|
+
if (tokens.length >= recordLimit || fieldTokenCount >= fieldLimit) break
|
|
109
|
+
const remainingLimit = Math.min(recordLimit - tokens.length, fieldLimit - fieldTokenCount)
|
|
110
|
+
const candidateLimit = fieldTokenCount + remainingLimit
|
|
111
|
+
const tokenConfig = Number.isFinite(candidateLimit)
|
|
112
|
+
? { ...config, maxTokensPerField: candidateLimit }
|
|
113
|
+
: config
|
|
114
|
+
const { tokens: textTokens, hashes } = tokenizeText(text, tokenConfig)
|
|
105
115
|
for (let i = 0; i < textTokens.length; i += 1) {
|
|
116
|
+
if (tokens.length >= recordLimit || fieldTokenCount >= fieldLimit) break
|
|
106
117
|
const token = textTokens[i]
|
|
107
118
|
const hash = hashes[i]
|
|
108
119
|
const dedupeKey = `${field}|${hash}`
|
|
109
120
|
if (seen.has(dedupeKey)) continue
|
|
110
121
|
seen.add(dedupeKey)
|
|
122
|
+
fieldTokenCount += 1
|
|
111
123
|
debug('token.generated', { entityType: params.entityType, recordId: params.recordId, field, hash })
|
|
112
124
|
tokens.push({
|
|
113
125
|
entity_type: params.entityType,
|
|
@@ -11,6 +11,7 @@ import { getShippingAdapter } from '../../../lib/adapter-registry'
|
|
|
11
11
|
import { getShippingCarrierQueue } from '../../../lib/queue'
|
|
12
12
|
import { shippingCarriersTag } from '../../openapi'
|
|
13
13
|
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
14
|
+
import { readBoundedRequestBody, WebhookBodyTooLargeError } from '@open-mercato/shared/lib/webhooks'
|
|
14
15
|
|
|
15
16
|
const logger = createLogger('shipping_carriers').child({ component: 'webhook' })
|
|
16
17
|
|
|
@@ -51,7 +52,15 @@ export async function POST(req: Request, { params }: { params: Promise<{ provide
|
|
|
51
52
|
const rateLimitResponse = await checkProviderWebhookRateLimit(container, req, providerKey)
|
|
52
53
|
if (rateLimitResponse) return rateLimitResponse
|
|
53
54
|
|
|
54
|
-
|
|
55
|
+
let rawBody: string
|
|
56
|
+
try {
|
|
57
|
+
rawBody = await readBoundedRequestBody(req)
|
|
58
|
+
} catch (error) {
|
|
59
|
+
if (error instanceof WebhookBodyTooLargeError) {
|
|
60
|
+
return NextResponse.json({ error: 'Webhook payload too large' }, { status: 413 })
|
|
61
|
+
}
|
|
62
|
+
throw error
|
|
63
|
+
}
|
|
55
64
|
const headers: Record<string, string> = {}
|
|
56
65
|
req.headers.forEach((value, key) => {
|
|
57
66
|
headers[key] = value
|
|
@@ -156,6 +165,7 @@ export const openApi = {
|
|
|
156
165
|
responses: [
|
|
157
166
|
{ status: 202, description: 'Webhook accepted for async processing' },
|
|
158
167
|
{ status: 401, description: 'Signature verification failed' },
|
|
168
|
+
{ status: 413, description: 'Webhook payload too large' },
|
|
159
169
|
{ status: 404, description: 'Unknown provider' },
|
|
160
170
|
],
|
|
161
171
|
},
|