@open-mercato/onboarding 0.6.6-develop.5412.1.e2a52b14f0 → 0.6.6-develop.5431.1.384a97c7a2
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/modules/onboarding/__integration__/TC-ONB-001-self-service-consent.spec.js +141 -0
- package/dist/modules/onboarding/__integration__/TC-ONB-001-self-service-consent.spec.js.map +7 -0
- package/dist/modules/onboarding/api/get/onboarding/status.js +34 -17
- package/dist/modules/onboarding/api/get/onboarding/status.js.map +2 -2
- package/dist/modules/onboarding/api/get/onboarding/verify.js +77 -195
- package/dist/modules/onboarding/api/get/onboarding/verify.js.map +2 -2
- package/dist/modules/onboarding/api/post/onboarding.js +10 -1
- package/dist/modules/onboarding/api/post/onboarding.js.map +2 -2
- package/dist/modules/onboarding/frontend/onboarding/OnboardingPageClient.js +31 -1
- package/dist/modules/onboarding/frontend/onboarding/OnboardingPageClient.js.map +2 -2
- package/dist/modules/onboarding/frontend/onboarding/preparing/PreparingPageClient.js +36 -7
- package/dist/modules/onboarding/frontend/onboarding/preparing/PreparingPageClient.js.map +2 -2
- package/dist/modules/onboarding/lib/deferred-provisioning.js +219 -0
- package/dist/modules/onboarding/lib/deferred-provisioning.js.map +7 -0
- package/dist/modules/onboarding/lib/provisioning.js +18 -0
- package/dist/modules/onboarding/lib/provisioning.js.map +7 -0
- package/dist/modules/onboarding/lib/verify-base-url.js +86 -0
- package/dist/modules/onboarding/lib/verify-base-url.js.map +7 -0
- package/package.json +3 -3
- package/src/__tests__/onboarding-submit-rate-limit.test.ts +22 -0
- package/src/__tests__/provisioning.test.ts +89 -0
- package/src/__tests__/status-endpoint-auth.test.ts +92 -3
- package/src/__tests__/verify-base-url.test.ts +65 -0
- package/src/modules/onboarding/__integration__/TC-ONB-001-self-service-consent.spec.ts +176 -0
- package/src/modules/onboarding/api/get/onboarding/status.ts +36 -17
- package/src/modules/onboarding/api/get/onboarding/verify.ts +96 -227
- package/src/modules/onboarding/api/post/onboarding.ts +9 -0
- package/src/modules/onboarding/frontend/onboarding/OnboardingPageClient.tsx +32 -2
- package/src/modules/onboarding/frontend/onboarding/preparing/PreparingPageClient.tsx +38 -6
- package/src/modules/onboarding/i18n/de.json +10 -1
- package/src/modules/onboarding/i18n/en.json +10 -1
- package/src/modules/onboarding/i18n/es.json +10 -1
- package/src/modules/onboarding/i18n/pl.json +10 -1
- package/src/modules/onboarding/lib/deferred-provisioning.ts +258 -0
- package/src/modules/onboarding/lib/provisioning.ts +35 -0
- package/src/modules/onboarding/lib/verify-base-url.ts +107 -0
|
@@ -77,7 +77,10 @@
|
|
|
77
77
|
"onboarding.preparing.descriptionWithTenant": "Wir schließen die Vorbereitung der Demo-Umgebung für {tenant} ab. Sobald alles bereit ist, senden wir Ihnen eine E-Mail mit dem korrekten Tenant-Login-Link.",
|
|
78
78
|
"onboarding.preparing.emailNotice": "Sie müssen diese Seite nicht geöffnet lassen. Wir senden Ihnen eine E-Mail, sobald alles bereit ist.",
|
|
79
79
|
"onboarding.preparing.homeCta": "Zur Startseite",
|
|
80
|
+
"onboarding.preparing.loginCta": "Tenant-Login öffnen",
|
|
80
81
|
"onboarding.preparing.redirecting": "Ihr Arbeitsbereich ist bereit. Wir leiten Sie jetzt zur Tenant-Anmeldung weiter.",
|
|
82
|
+
"onboarding.preparing.statusErrorBody": "Der Arbeitsbereichstatus konnte nicht geprüft werden. Der Arbeitsbereich wird möglicherweise noch vorbereitet; diese Seite versucht es weiter.",
|
|
83
|
+
"onboarding.preparing.statusErrorTitle": "Statusprüfung fehlgeschlagen",
|
|
81
84
|
"onboarding.preparing.title": "Wir bereiten Ihren Arbeitsbereich vor",
|
|
82
85
|
"onboarding.readyEmail.body": "Ihr Open Mercato-Arbeitsbereich für {organizationName} ist fertig vorbereitet. Verwenden Sie den sicheren Link unten, um sich anzumelden.",
|
|
83
86
|
"onboarding.readyEmail.cta": "Login öffnen",
|
|
@@ -87,5 +90,11 @@
|
|
|
87
90
|
"onboarding.readyEmail.preview": "Ihr Arbeitsbereich ist bereit. Verwenden Sie Ihren sicheren Login-Link zur Anmeldung.",
|
|
88
91
|
"onboarding.readyEmail.subject": "Ihr Open Mercato-Arbeitsbereich ist bereit",
|
|
89
92
|
"onboarding.subtitle": "Erzählen Sie uns ein wenig über sich und wir richten alles für Sie ein.",
|
|
90
|
-
"onboarding.title": "Erstellen Sie Ihren Open Mercato-Arbeitsbereich"
|
|
93
|
+
"onboarding.title": "Erstellen Sie Ihren Open Mercato-Arbeitsbereich",
|
|
94
|
+
"onboarding.verifyStatus.alreadyExists": "Es existiert bereits ein Konto mit dieser E-Mail-Adresse. Versuchen Sie, sich anzumelden oder Ihr Passwort zurückzusetzen.",
|
|
95
|
+
"onboarding.verifyStatus.error": "Die Verifizierung konnte nicht abgeschlossen werden. Bitte versuchen Sie es erneut oder kontaktieren Sie den Support.",
|
|
96
|
+
"onboarding.verifyStatus.invalid": "Der Verifizierungslink ist ungültig oder abgelaufen. Senden Sie das Onboarding-Formular erneut, um einen neuen Link zu erhalten.",
|
|
97
|
+
"onboarding.verifyStatus.originNotAllowed": "Der Verifizierungslink wurde von einem nicht erlaubten Origin geöffnet. Prüfen Sie APP_URL und APP_ALLOWED_ORIGINS und öffnen Sie den Link erneut.",
|
|
98
|
+
"onboarding.verifyStatus.redirectMisconfigured": "Der Verifizierungslink kann nicht sicher weiterleiten, weil APP_URL nicht zur URL passt, die die Anfrage verarbeitet hat. Prüfen Sie APP_URL und APP_ALLOWED_ORIGINS und öffnen Sie den Link erneut.",
|
|
99
|
+
"onboarding.verifyStatus.urlNotConfigured": "Die Onboarding-Verifizierung ist nicht konfiguriert. APP_URL muss gesetzt sein, bevor Verifizierungslinks verwendet werden können."
|
|
91
100
|
}
|
|
@@ -77,7 +77,10 @@
|
|
|
77
77
|
"onboarding.preparing.descriptionWithTenant": "We are finishing the demo environment for {tenant}. We will send you an email with the correct tenant login link as soon as it is ready.",
|
|
78
78
|
"onboarding.preparing.emailNotice": "You do not need to keep this page open. We will email you when everything is ready.",
|
|
79
79
|
"onboarding.preparing.homeCta": "Go to home page",
|
|
80
|
+
"onboarding.preparing.loginCta": "Open tenant login",
|
|
80
81
|
"onboarding.preparing.redirecting": "Your workspace is ready. Redirecting you to the tenant login page now.",
|
|
82
|
+
"onboarding.preparing.statusErrorBody": "We could not check the workspace status. The workspace may still be preparing; this page will keep retrying.",
|
|
83
|
+
"onboarding.preparing.statusErrorTitle": "Workspace status check failed",
|
|
81
84
|
"onboarding.preparing.title": "We are preparing your workspace",
|
|
82
85
|
"onboarding.readyEmail.body": "Your Open Mercato workspace for {organizationName} has finished preparing. Use the secure link below to sign in.",
|
|
83
86
|
"onboarding.readyEmail.cta": "Open login",
|
|
@@ -87,5 +90,11 @@
|
|
|
87
90
|
"onboarding.readyEmail.preview": "Your workspace is ready. Use your secure login link to sign in.",
|
|
88
91
|
"onboarding.readyEmail.subject": "Your Open Mercato workspace is ready",
|
|
89
92
|
"onboarding.subtitle": "Tell us a bit about you and we will set everything up.",
|
|
90
|
-
"onboarding.title": "Create your Open Mercato workspace"
|
|
93
|
+
"onboarding.title": "Create your Open Mercato workspace",
|
|
94
|
+
"onboarding.verifyStatus.alreadyExists": "We already have an account with this email. Try signing in or resetting your password.",
|
|
95
|
+
"onboarding.verifyStatus.error": "We could not complete verification. Please try again or contact support.",
|
|
96
|
+
"onboarding.verifyStatus.invalid": "The verification link is invalid or expired. Submit the onboarding form again to receive a new link.",
|
|
97
|
+
"onboarding.verifyStatus.originNotAllowed": "The verification link was opened from an origin that is not allowed. Check APP_URL and APP_ALLOWED_ORIGINS, then open the verification link again.",
|
|
98
|
+
"onboarding.verifyStatus.redirectMisconfigured": "The verification link cannot redirect safely because APP_URL does not match the URL that handled the request. Check APP_URL and APP_ALLOWED_ORIGINS, then open the verification link again.",
|
|
99
|
+
"onboarding.verifyStatus.urlNotConfigured": "Onboarding verification is not configured. APP_URL must be set before verification links can be used."
|
|
91
100
|
}
|
|
@@ -77,7 +77,10 @@
|
|
|
77
77
|
"onboarding.preparing.descriptionWithTenant": "Estamos terminando de preparar el entorno de demostración para {tenant}. Le enviaremos un correo con el enlace correcto de acceso al tenant en cuanto esté listo.",
|
|
78
78
|
"onboarding.preparing.emailNotice": "No necesita mantener esta página abierta. Le enviaremos un correo cuando todo esté listo.",
|
|
79
79
|
"onboarding.preparing.homeCta": "Ir a la página principal",
|
|
80
|
+
"onboarding.preparing.loginCta": "Abrir acceso al tenant",
|
|
80
81
|
"onboarding.preparing.redirecting": "Su espacio de trabajo ya está listo. Ahora le redirigimos a la página de acceso del tenant.",
|
|
82
|
+
"onboarding.preparing.statusErrorBody": "No pudimos comprobar el estado del espacio de trabajo. Es posible que aún se esté preparando; esta página seguirá intentándolo.",
|
|
83
|
+
"onboarding.preparing.statusErrorTitle": "Falló la comprobación de estado",
|
|
81
84
|
"onboarding.preparing.title": "Estamos preparando su espacio de trabajo",
|
|
82
85
|
"onboarding.readyEmail.body": "Su espacio de trabajo de Open Mercato para {organizationName} ya está listo. Use el enlace seguro de abajo para iniciar sesión.",
|
|
83
86
|
"onboarding.readyEmail.cta": "Abrir acceso",
|
|
@@ -87,5 +90,11 @@
|
|
|
87
90
|
"onboarding.readyEmail.preview": "Su espacio de trabajo está listo. Use su enlace seguro de acceso para iniciar sesión.",
|
|
88
91
|
"onboarding.readyEmail.subject": "Su espacio de trabajo de Open Mercato está listo",
|
|
89
92
|
"onboarding.subtitle": "Cuéntenos un poco sobre usted y configuraremos todo.",
|
|
90
|
-
"onboarding.title": "Cree su espacio de trabajo en Open Mercato"
|
|
93
|
+
"onboarding.title": "Cree su espacio de trabajo en Open Mercato",
|
|
94
|
+
"onboarding.verifyStatus.alreadyExists": "Ya tenemos una cuenta con este correo electrónico. Intente iniciar sesión o restablecer su contraseña.",
|
|
95
|
+
"onboarding.verifyStatus.error": "No pudimos completar la verificación. Intente de nuevo o comuníquese con soporte.",
|
|
96
|
+
"onboarding.verifyStatus.invalid": "El enlace de verificación no es válido o caducó. Envíe de nuevo el formulario de onboarding para recibir un enlace nuevo.",
|
|
97
|
+
"onboarding.verifyStatus.originNotAllowed": "El enlace de verificación se abrió desde un origen no permitido. Revise APP_URL y APP_ALLOWED_ORIGINS y abra el enlace de nuevo.",
|
|
98
|
+
"onboarding.verifyStatus.redirectMisconfigured": "El enlace de verificación no puede redirigir de forma segura porque APP_URL no coincide con la URL que procesó la solicitud. Revise APP_URL y APP_ALLOWED_ORIGINS y abra el enlace de nuevo.",
|
|
99
|
+
"onboarding.verifyStatus.urlNotConfigured": "La verificación de onboarding no está configurada. APP_URL debe estar definido antes de usar enlaces de verificación."
|
|
91
100
|
}
|
|
@@ -77,7 +77,10 @@
|
|
|
77
77
|
"onboarding.preparing.descriptionWithTenant": "Kończymy przygotowywać środowisko demo dla {tenant}. Gdy będzie gotowe, wyślemy Ci e-mail z poprawnym linkiem logowania do tenanta.",
|
|
78
78
|
"onboarding.preparing.emailNotice": "Nie musisz zostawiać tej strony otwartej. Gdy wszystko będzie gotowe, wyślemy Ci e-mail.",
|
|
79
79
|
"onboarding.preparing.homeCta": "Przejdź na stronę główną",
|
|
80
|
+
"onboarding.preparing.loginCta": "Otwórz logowanie do tenanta",
|
|
80
81
|
"onboarding.preparing.redirecting": "Twój obszar roboczy jest gotowy. Przekierowujemy Cię teraz do strony logowania tenanta.",
|
|
82
|
+
"onboarding.preparing.statusErrorBody": "Nie udało się sprawdzić statusu obszaru roboczego. Obszar może nadal się przygotowywać; ta strona będzie ponawiać próbę.",
|
|
83
|
+
"onboarding.preparing.statusErrorTitle": "Nie udało się sprawdzić statusu obszaru",
|
|
81
84
|
"onboarding.preparing.title": "Przygotowujemy Twój obszar roboczy",
|
|
82
85
|
"onboarding.readyEmail.body": "Twój obszar roboczy Open Mercato dla {organizationName} jest już gotowy. Użyj bezpiecznego linku poniżej, aby się zalogować.",
|
|
83
86
|
"onboarding.readyEmail.cta": "Otwórz logowanie",
|
|
@@ -87,5 +90,11 @@
|
|
|
87
90
|
"onboarding.readyEmail.preview": "Twój obszar roboczy jest gotowy. Użyj bezpiecznego linku logowania, aby się zalogować.",
|
|
88
91
|
"onboarding.readyEmail.subject": "Twój obszar roboczy Open Mercato jest gotowy",
|
|
89
92
|
"onboarding.subtitle": "Opowiedz nam trochę o sobie, a my wszystko skonfigurujemy.",
|
|
90
|
-
"onboarding.title": "Utwórz swój obszar roboczy Open Mercato"
|
|
93
|
+
"onboarding.title": "Utwórz swój obszar roboczy Open Mercato",
|
|
94
|
+
"onboarding.verifyStatus.alreadyExists": "Mamy już konto z tym adresem email. Spróbuj się zalogować lub zresetować hasło.",
|
|
95
|
+
"onboarding.verifyStatus.error": "Nie udało się zakończyć weryfikacji. Spróbuj ponownie lub skontaktuj się z pomocą techniczną.",
|
|
96
|
+
"onboarding.verifyStatus.invalid": "Link weryfikacyjny jest nieprawidłowy albo wygasł. Wyślij formularz onboardingu ponownie, aby otrzymać nowy link.",
|
|
97
|
+
"onboarding.verifyStatus.originNotAllowed": "Link weryfikacyjny otwarto z niedozwolonego originu. Sprawdź APP_URL i APP_ALLOWED_ORIGINS, a potem otwórz link ponownie.",
|
|
98
|
+
"onboarding.verifyStatus.redirectMisconfigured": "Link weryfikacyjny nie może bezpiecznie przekierować, bo APP_URL nie zgadza się z URL-em, który obsłużył request. Sprawdź APP_URL i APP_ALLOWED_ORIGINS, a potem otwórz link ponownie.",
|
|
99
|
+
"onboarding.verifyStatus.urlNotConfigured": "Weryfikacja onboardingu nie jest skonfigurowana. APP_URL musi być ustawiony, zanim linki weryfikacyjne będą działać."
|
|
91
100
|
}
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
2
|
+
import type { SearchIndexer } from '@open-mercato/search'
|
|
3
|
+
import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
4
|
+
import { flattenSystemEntityIds } from '@open-mercato/shared/lib/entities/system-entities'
|
|
5
|
+
import { getEntityIds } from '@open-mercato/shared/lib/encryption/entityIds'
|
|
6
|
+
import { getModules } from '@open-mercato/shared/lib/modules/registry'
|
|
7
|
+
import type { OnboardingRequest } from '@open-mercato/onboarding/modules/onboarding/data/entities'
|
|
8
|
+
import { OnboardingService } from '@open-mercato/onboarding/modules/onboarding/lib/service'
|
|
9
|
+
import { sendWorkspaceReadyEmail } from '@open-mercato/onboarding/modules/onboarding/lib/ready-email'
|
|
10
|
+
import { reindexEntity } from '@open-mercato/core/modules/query_index/lib/reindexer'
|
|
11
|
+
import { purgeIndexScope } from '@open-mercato/core/modules/query_index/lib/purge'
|
|
12
|
+
import { refreshCoverageSnapshot } from '@open-mercato/core/modules/query_index/lib/coverage'
|
|
13
|
+
import { isUniqueViolation } from '@open-mercato/shared/lib/crud/errors'
|
|
14
|
+
|
|
15
|
+
const VECTOR_REINDEX_ENQUEUE_TIMEOUT_MS = 5_000
|
|
16
|
+
const SEED_EXAMPLES_TIMEOUT_MS = 15_000
|
|
17
|
+
|
|
18
|
+
export function resolveProvisioningIds(request: OnboardingRequest) {
|
|
19
|
+
if (!request.tenantId || !request.organizationId || !request.userId) return null
|
|
20
|
+
return {
|
|
21
|
+
tenantId: request.tenantId,
|
|
22
|
+
organizationId: request.organizationId,
|
|
23
|
+
userId: request.userId,
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function createTimeoutPromise(label: string, timeoutMs: number): Promise<never> {
|
|
28
|
+
return new Promise((_, reject) => {
|
|
29
|
+
setTimeout(() => reject(new Error(`${label} timed out after ${timeoutMs}ms`)), timeoutMs)
|
|
30
|
+
})
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function runModuleSetupHook(args: {
|
|
34
|
+
moduleId: string
|
|
35
|
+
phase: 'seedExamples'
|
|
36
|
+
timeoutMs: number
|
|
37
|
+
run: () => Promise<void>
|
|
38
|
+
}) {
|
|
39
|
+
const startedAt = Date.now()
|
|
40
|
+
console.info('[onboarding.verify] module hook started', {
|
|
41
|
+
moduleId: args.moduleId,
|
|
42
|
+
phase: args.phase,
|
|
43
|
+
timeoutMs: args.timeoutMs,
|
|
44
|
+
})
|
|
45
|
+
try {
|
|
46
|
+
await Promise.race([
|
|
47
|
+
args.run(),
|
|
48
|
+
createTimeoutPromise(`module ${args.moduleId} ${args.phase}`, args.timeoutMs),
|
|
49
|
+
])
|
|
50
|
+
console.info('[onboarding.verify] module hook completed', {
|
|
51
|
+
moduleId: args.moduleId,
|
|
52
|
+
phase: args.phase,
|
|
53
|
+
durationMs: Math.max(0, Date.now() - startedAt),
|
|
54
|
+
})
|
|
55
|
+
} catch (error) {
|
|
56
|
+
if (isUniqueViolation(error)) {
|
|
57
|
+
// Deferred provisioning is re-triggered on every preparing-page status
|
|
58
|
+
// poll until preparationCompletedAt is set. seedExamples is not fully
|
|
59
|
+
// idempotent (e.g. catalog product handles are unique-scoped), so a
|
|
60
|
+
// re-run that lands before completion collides on an already-seeded row.
|
|
61
|
+
// The workspace is already provisioned and the collision is expected and
|
|
62
|
+
// harmless — log at info so genuine failures still stand out.
|
|
63
|
+
console.info('[onboarding.verify] module hook skipped (already seeded)', {
|
|
64
|
+
moduleId: args.moduleId,
|
|
65
|
+
phase: args.phase,
|
|
66
|
+
durationMs: Math.max(0, Date.now() - startedAt),
|
|
67
|
+
})
|
|
68
|
+
} else {
|
|
69
|
+
console.error('[onboarding.verify] module hook failed', {
|
|
70
|
+
moduleId: args.moduleId,
|
|
71
|
+
phase: args.phase,
|
|
72
|
+
durationMs: Math.max(0, Date.now() - startedAt),
|
|
73
|
+
timeoutMs: args.timeoutMs,
|
|
74
|
+
error,
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
throw error
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function markWorkspaceReady(args: {
|
|
82
|
+
requestId: string
|
|
83
|
+
}) {
|
|
84
|
+
const container = await createRequestContainer()
|
|
85
|
+
const em = container.resolve('em') as EntityManager
|
|
86
|
+
const service = new OnboardingService(em)
|
|
87
|
+
const request = await service.findById(args.requestId)
|
|
88
|
+
if (!request || request.preparationCompletedAt) return
|
|
89
|
+
await service.markPreparationCompleted(request, new Date())
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function enqueueVectorReindex(args: {
|
|
93
|
+
container: { resolve: <T = unknown>(name: string) => T }
|
|
94
|
+
tenantId: string
|
|
95
|
+
organizationId: string
|
|
96
|
+
}) {
|
|
97
|
+
let searchIndexer: SearchIndexer | null = null
|
|
98
|
+
try {
|
|
99
|
+
searchIndexer = args.container.resolve<SearchIndexer>('searchIndexer')
|
|
100
|
+
} catch {
|
|
101
|
+
searchIndexer = null
|
|
102
|
+
}
|
|
103
|
+
if (!searchIndexer) return
|
|
104
|
+
|
|
105
|
+
await Promise.race([
|
|
106
|
+
searchIndexer.reindexAllToVector({
|
|
107
|
+
tenantId: args.tenantId,
|
|
108
|
+
organizationId: args.organizationId,
|
|
109
|
+
purgeFirst: true,
|
|
110
|
+
useQueue: true,
|
|
111
|
+
}),
|
|
112
|
+
createTimeoutPromise('vector reindex enqueue', VECTOR_REINDEX_ENQUEUE_TIMEOUT_MS),
|
|
113
|
+
])
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function rebuildTenantQueryIndexes(args: {
|
|
117
|
+
em: EntityManager
|
|
118
|
+
tenantId: string
|
|
119
|
+
organizationId: string
|
|
120
|
+
}) {
|
|
121
|
+
const coverageRefreshKeys = new Set<string>()
|
|
122
|
+
try {
|
|
123
|
+
const allEntities = getEntityIds()
|
|
124
|
+
const entityIds = flattenSystemEntityIds(allEntities)
|
|
125
|
+
for (const entityType of entityIds) {
|
|
126
|
+
try {
|
|
127
|
+
await purgeIndexScope(args.em, { entityType, tenantId: args.tenantId })
|
|
128
|
+
} catch (error) {
|
|
129
|
+
console.error('[onboarding.verify] failed to purge query index scope', {
|
|
130
|
+
entityType,
|
|
131
|
+
tenantId: args.tenantId,
|
|
132
|
+
error,
|
|
133
|
+
})
|
|
134
|
+
}
|
|
135
|
+
try {
|
|
136
|
+
await reindexEntity(args.em, {
|
|
137
|
+
entityType,
|
|
138
|
+
tenantId: args.tenantId,
|
|
139
|
+
force: true,
|
|
140
|
+
emitVectorizeEvents: false,
|
|
141
|
+
vectorService: null,
|
|
142
|
+
})
|
|
143
|
+
} catch (error) {
|
|
144
|
+
console.error('[onboarding.verify] failed to reindex entity', {
|
|
145
|
+
entityType,
|
|
146
|
+
tenantId: args.tenantId,
|
|
147
|
+
error,
|
|
148
|
+
})
|
|
149
|
+
}
|
|
150
|
+
coverageRefreshKeys.add(`${entityType}|${args.tenantId}|__null__`)
|
|
151
|
+
coverageRefreshKeys.add(`${entityType}|${args.tenantId}|${args.organizationId}`)
|
|
152
|
+
}
|
|
153
|
+
} catch (error) {
|
|
154
|
+
console.error('[onboarding.verify] failed to rebuild query indexes', { tenantId: args.tenantId, error })
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (!coverageRefreshKeys.size) return
|
|
158
|
+
|
|
159
|
+
for (const entry of coverageRefreshKeys) {
|
|
160
|
+
const [entityType, tenantKey, orgKey] = entry.split('|')
|
|
161
|
+
const orgScope = orgKey === '__null__' ? null : orgKey
|
|
162
|
+
try {
|
|
163
|
+
await refreshCoverageSnapshot(
|
|
164
|
+
args.em,
|
|
165
|
+
{
|
|
166
|
+
entityType,
|
|
167
|
+
tenantId: tenantKey,
|
|
168
|
+
organizationId: orgScope,
|
|
169
|
+
withDeleted: false,
|
|
170
|
+
},
|
|
171
|
+
)
|
|
172
|
+
} catch (error) {
|
|
173
|
+
console.error('[onboarding.verify] failed to refresh coverage snapshot', {
|
|
174
|
+
entityType,
|
|
175
|
+
tenantId: tenantKey,
|
|
176
|
+
organizationId: orgScope,
|
|
177
|
+
error,
|
|
178
|
+
})
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export async function runDeferredProvisioning(args: {
|
|
184
|
+
requestId: string
|
|
185
|
+
tenantId: string
|
|
186
|
+
organizationId: string
|
|
187
|
+
}) {
|
|
188
|
+
const container = await createRequestContainer()
|
|
189
|
+
const em = container.resolve('em') as EntityManager
|
|
190
|
+
const modules = getModules()
|
|
191
|
+
|
|
192
|
+
for (const mod of modules) {
|
|
193
|
+
if (!mod.setup?.seedExamples) continue
|
|
194
|
+
try {
|
|
195
|
+
await runModuleSetupHook({
|
|
196
|
+
moduleId: mod.id,
|
|
197
|
+
phase: 'seedExamples',
|
|
198
|
+
timeoutMs: SEED_EXAMPLES_TIMEOUT_MS,
|
|
199
|
+
run: () => mod.setup!.seedExamples!({
|
|
200
|
+
em,
|
|
201
|
+
tenantId: args.tenantId,
|
|
202
|
+
organizationId: args.organizationId,
|
|
203
|
+
container,
|
|
204
|
+
}),
|
|
205
|
+
})
|
|
206
|
+
} catch (error) {
|
|
207
|
+
if (isUniqueViolation(error)) {
|
|
208
|
+
console.info('[onboarding.verify] deferred seedExamples skipped (already applied)', {
|
|
209
|
+
moduleId: mod.id,
|
|
210
|
+
tenantId: args.tenantId,
|
|
211
|
+
organizationId: args.organizationId,
|
|
212
|
+
})
|
|
213
|
+
} else {
|
|
214
|
+
console.error('[onboarding.verify] deferred seedExamples failed', {
|
|
215
|
+
moduleId: mod.id,
|
|
216
|
+
tenantId: args.tenantId,
|
|
217
|
+
organizationId: args.organizationId,
|
|
218
|
+
error,
|
|
219
|
+
})
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
await markWorkspaceReady({
|
|
225
|
+
requestId: args.requestId,
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
await sendWorkspaceReadyEmail({
|
|
229
|
+
requestId: args.requestId,
|
|
230
|
+
tenantId: args.tenantId,
|
|
231
|
+
}).catch((error) => {
|
|
232
|
+
console.error('[onboarding.verify] ready email failed', {
|
|
233
|
+
requestId: args.requestId,
|
|
234
|
+
tenantId: args.tenantId,
|
|
235
|
+
organizationId: args.organizationId,
|
|
236
|
+
error,
|
|
237
|
+
})
|
|
238
|
+
throw error
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
await rebuildTenantQueryIndexes({
|
|
242
|
+
em,
|
|
243
|
+
tenantId: args.tenantId,
|
|
244
|
+
organizationId: args.organizationId,
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
await enqueueVectorReindex({
|
|
248
|
+
container,
|
|
249
|
+
tenantId: args.tenantId,
|
|
250
|
+
organizationId: args.organizationId,
|
|
251
|
+
}).catch((error) => {
|
|
252
|
+
console.warn('[onboarding.verify] vector reindex enqueue did not complete promptly', {
|
|
253
|
+
tenantId: args.tenantId,
|
|
254
|
+
organizationId: args.organizationId,
|
|
255
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
256
|
+
})
|
|
257
|
+
})
|
|
258
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { isUniqueViolation } from '@open-mercato/shared/lib/crud/errors'
|
|
2
|
+
|
|
3
|
+
export type BestEffortProvisioningResult = { ok: true } | { ok: false; error: unknown }
|
|
4
|
+
|
|
5
|
+
type BestEffortLogger = Pick<Console, 'error' | 'info'>
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Runs a non-essential provisioning step without letting its failure abort
|
|
9
|
+
* onboarding. The tenant/org/primary user are created before these steps, so a
|
|
10
|
+
* throw or timeout here (reference-data seeding, marketing-consent recording)
|
|
11
|
+
* must NOT strand a freshly provisioned workspace — the verify handler still has
|
|
12
|
+
* to reach markCompleted so the user can sign in. Failures are logged for
|
|
13
|
+
* follow-up and reported back to the caller, never re-thrown.
|
|
14
|
+
*/
|
|
15
|
+
export async function runBestEffortProvisioningStep(
|
|
16
|
+
step: string,
|
|
17
|
+
run: () => Promise<void>,
|
|
18
|
+
logger: BestEffortLogger = console,
|
|
19
|
+
): Promise<BestEffortProvisioningResult> {
|
|
20
|
+
try {
|
|
21
|
+
await run()
|
|
22
|
+
return { ok: true }
|
|
23
|
+
} catch (error) {
|
|
24
|
+
if (isUniqueViolation(error)) {
|
|
25
|
+
// Expected when a concurrent verify / re-verify re-applies a step against
|
|
26
|
+
// rows that already exist (seed hooks are not fully idempotent). The
|
|
27
|
+
// workspace is already provisioned, so the collision is harmless — log at
|
|
28
|
+
// info to keep genuine non-fatal failures visible.
|
|
29
|
+
logger.info('[onboarding.verify] non-fatal provisioning step skipped (already applied)', { step })
|
|
30
|
+
return { ok: false, error }
|
|
31
|
+
}
|
|
32
|
+
logger.error('[onboarding.verify] non-fatal provisioning step failed', { step, error })
|
|
33
|
+
return { ok: false, error }
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AppOriginConfigurationError,
|
|
3
|
+
AppOriginRejectedError,
|
|
4
|
+
getSecurityEmailBaseUrl,
|
|
5
|
+
resolveRequestOrigin,
|
|
6
|
+
} from '@open-mercato/shared/lib/url'
|
|
7
|
+
|
|
8
|
+
type EnvLike = Record<string, string | undefined>
|
|
9
|
+
|
|
10
|
+
export type VerifyRedirectBaseUrlResult =
|
|
11
|
+
| { ok: true; baseUrl: string }
|
|
12
|
+
| {
|
|
13
|
+
ok: false
|
|
14
|
+
status: 'origin_not_allowed' | 'redirect_misconfigured' | 'url_not_configured'
|
|
15
|
+
message: string
|
|
16
|
+
redirectOrigin: string | null
|
|
17
|
+
httpStatus: number
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function normalizeOrigin(raw: string): string | null {
|
|
21
|
+
try {
|
|
22
|
+
return new URL(raw).origin
|
|
23
|
+
} catch {
|
|
24
|
+
return null
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function originPort(url: URL): string {
|
|
29
|
+
if (url.port) return url.port
|
|
30
|
+
if (url.protocol === 'https:') return '443'
|
|
31
|
+
if (url.protocol === 'http:') return '80'
|
|
32
|
+
return ''
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function isLoopbackHostname(hostname: string): boolean {
|
|
36
|
+
return hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1'
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function originsMatchForRedirect(configuredOrigin: string, requestOrigin: string): boolean {
|
|
40
|
+
if (configuredOrigin === requestOrigin) return true
|
|
41
|
+
try {
|
|
42
|
+
const configured = new URL(configuredOrigin)
|
|
43
|
+
const request = new URL(requestOrigin)
|
|
44
|
+
return (
|
|
45
|
+
configured.protocol === request.protocol &&
|
|
46
|
+
isLoopbackHostname(configured.hostname) &&
|
|
47
|
+
isLoopbackHostname(request.hostname) &&
|
|
48
|
+
originPort(configured) === originPort(request)
|
|
49
|
+
)
|
|
50
|
+
} catch {
|
|
51
|
+
return false
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function resolveVerifyRedirectBaseUrl(
|
|
56
|
+
req: Request,
|
|
57
|
+
env: EnvLike = process.env,
|
|
58
|
+
): VerifyRedirectBaseUrlResult {
|
|
59
|
+
try {
|
|
60
|
+
const baseUrl = getSecurityEmailBaseUrl(req, env)
|
|
61
|
+
const configuredOrigin = normalizeOrigin(baseUrl)
|
|
62
|
+
const requestOrigin = normalizeOrigin(resolveRequestOrigin(req)) ?? normalizeOrigin(req.url)
|
|
63
|
+
if (configuredOrigin && requestOrigin && !originsMatchForRedirect(configuredOrigin, requestOrigin)) {
|
|
64
|
+
console.error('[onboarding.verify] APP_URL does not match verification request origin', {
|
|
65
|
+
requestUrl: req.url,
|
|
66
|
+
configuredOrigin,
|
|
67
|
+
requestOrigin,
|
|
68
|
+
})
|
|
69
|
+
return {
|
|
70
|
+
ok: false,
|
|
71
|
+
status: 'redirect_misconfigured',
|
|
72
|
+
message: 'Onboarding verification redirect is misconfigured.',
|
|
73
|
+
redirectOrigin: requestOrigin,
|
|
74
|
+
httpStatus: 400,
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return { ok: true, baseUrl }
|
|
78
|
+
} catch (error) {
|
|
79
|
+
if (error instanceof AppOriginRejectedError) {
|
|
80
|
+
console.error('[onboarding.verify] rejected request origin for redirect base', {
|
|
81
|
+
requestUrl: req.url,
|
|
82
|
+
reason: error.message,
|
|
83
|
+
})
|
|
84
|
+
return {
|
|
85
|
+
ok: false,
|
|
86
|
+
status: 'origin_not_allowed',
|
|
87
|
+
message: 'Onboarding verification request origin is not allowed.',
|
|
88
|
+
redirectOrigin: null,
|
|
89
|
+
httpStatus: 400,
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (error instanceof AppOriginConfigurationError) {
|
|
93
|
+
console.error('[onboarding.verify] APP_URL is required for onboarding verification redirects', {
|
|
94
|
+
requestUrl: req.url,
|
|
95
|
+
reason: error.message,
|
|
96
|
+
})
|
|
97
|
+
return {
|
|
98
|
+
ok: false,
|
|
99
|
+
status: 'url_not_configured',
|
|
100
|
+
message: 'Onboarding verification redirect is not configured.',
|
|
101
|
+
redirectOrigin: null,
|
|
102
|
+
httpStatus: 500,
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
throw error
|
|
106
|
+
}
|
|
107
|
+
}
|