@open-mercato/onboarding 0.6.5-develop.5382.1.f542de69af → 0.6.5
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/__integration__/TC-ONB-002-multi-tenant-parallel-login.spec.js +158 -0
- package/dist/modules/onboarding/__integration__/TC-ONB-002-multi-tenant-parallel-login.spec.js.map +7 -0
- package/dist/modules/onboarding/api/get/onboarding/status.js +35 -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/data/entities.js +3 -0
- package/dist/modules/onboarding/data/entities.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 +235 -0
- package/dist/modules/onboarding/lib/deferred-provisioning.js.map +7 -0
- package/dist/modules/onboarding/lib/preparation-claim.js +10 -0
- package/dist/modules/onboarding/lib/preparation-claim.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/service.js +31 -0
- package/dist/modules/onboarding/lib/service.js.map +2 -2
- package/dist/modules/onboarding/lib/verify-base-url.js +86 -0
- package/dist/modules/onboarding/lib/verify-base-url.js.map +7 -0
- package/dist/modules/onboarding/migrations/Migration20260611120000.js +13 -0
- package/dist/modules/onboarding/migrations/Migration20260611120000.js.map +7 -0
- package/generated/entities/onboarding_request/index.ts +1 -0
- package/generated/entity-fields-registry.ts +1 -0
- package/package.json +4 -5
- package/src/__tests__/deferred-provisioning.test.ts +210 -0
- 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 +130 -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/__integration__/TC-ONB-002-multi-tenant-parallel-login.spec.ts +198 -0
- package/src/modules/onboarding/api/get/onboarding/status.ts +46 -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/data/entities.ts +3 -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 +295 -0
- package/src/modules/onboarding/lib/preparation-claim.ts +6 -0
- package/src/modules/onboarding/lib/provisioning.ts +35 -0
- package/src/modules/onboarding/lib/service.ts +33 -0
- package/src/modules/onboarding/lib/verify-base-url.ts +107 -0
- package/src/modules/onboarding/migrations/.snapshot-open-mercato.json +10 -0
- package/src/modules/onboarding/migrations/Migration20260611120000.ts +13 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/onboarding/data/entities.ts"],
|
|
4
|
-
"sourcesContent": ["import { Entity, PrimaryKey, Property, Unique } from '@mikro-orm/decorators/legacy'\n\ntype OnboardingStatus = 'pending' | 'processing' | 'completed' | 'expired'\n\n@Entity({ tableName: 'onboarding_requests' })\n@Unique({ properties: ['email'] })\n@Unique({ properties: ['tokenHash'] })\nexport class OnboardingRequest {\n @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })\n id!: string\n\n @Property({ type: 'text' })\n email!: string\n\n @Property({ name: 'token_hash', type: 'text' })\n tokenHash!: string\n\n @Property({ type: 'text', default: 'pending' })\n status: OnboardingStatus = 'pending'\n\n @Property({ name: 'first_name', type: 'text' })\n firstName!: string\n\n @Property({ name: 'last_name', type: 'text' })\n lastName!: string\n\n @Property({ name: 'organization_name', type: 'text' })\n organizationName!: string\n\n @Property({ type: 'text', nullable: true })\n locale?: string | null\n\n @Property({ name: 'terms_accepted', type: 'boolean', default: false })\n termsAccepted: boolean = false\n\n @Property({ name: 'marketing_consent', type: 'boolean', default: false, nullable: true })\n marketingConsent?: boolean | null = false\n\n @Property({ name: 'password_hash', type: 'text', nullable: true })\n passwordHash?: string | null\n\n @Property({ name: 'processing_started_at', type: Date, nullable: true })\n processingStartedAt?: Date | null\n\n @Property({ name: 'expires_at', type: Date })\n expiresAt!: Date\n\n @Property({ name: 'completed_at', type: Date, nullable: true })\n completedAt?: Date | null\n\n @Property({ name: 'tenant_id', type: 'uuid', nullable: true })\n tenantId?: string | null\n\n @Property({ name: 'organization_id', type: 'uuid', nullable: true })\n organizationId?: string | null\n\n @Property({ name: 'user_id', type: 'uuid', nullable: true })\n userId?: string | null\n\n @Property({ name: 'last_email_sent_at', type: Date, nullable: true })\n lastEmailSentAt?: Date | null\n\n @Property({ name: 'preparation_completed_at', type: Date, nullable: true })\n preparationCompletedAt?: Date | null\n\n @Property({ name: 'ready_email_sent_at', type: Date, nullable: true })\n readyEmailSentAt?: Date | null\n\n @Property({ name: 'created_at', type: Date, onCreate: () => new Date() })\n createdAt: Date = new Date()\n\n @Property({ name: 'updated_at', type: Date, onUpdate: () => new Date(), nullable: true })\n updatedAt?: Date\n\n @Property({ name: 'deleted_at', type: Date, nullable: true })\n deletedAt?: Date | null\n}\n\nexport type { OnboardingStatus }\n"],
|
|
5
|
-
"mappings": ";;;;;;;;;;AAAA,SAAS,QAAQ,YAAY,UAAU,cAAc;AAO9C,IAAM,oBAAN,MAAwB;AAAA,EAAxB;AAWL,kBAA2B;AAe3B,yBAAyB;AAGzB,4BAAoC;
|
|
4
|
+
"sourcesContent": ["import { Entity, PrimaryKey, Property, Unique } from '@mikro-orm/decorators/legacy'\n\ntype OnboardingStatus = 'pending' | 'processing' | 'completed' | 'expired'\n\n@Entity({ tableName: 'onboarding_requests' })\n@Unique({ properties: ['email'] })\n@Unique({ properties: ['tokenHash'] })\nexport class OnboardingRequest {\n @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })\n id!: string\n\n @Property({ type: 'text' })\n email!: string\n\n @Property({ name: 'token_hash', type: 'text' })\n tokenHash!: string\n\n @Property({ type: 'text', default: 'pending' })\n status: OnboardingStatus = 'pending'\n\n @Property({ name: 'first_name', type: 'text' })\n firstName!: string\n\n @Property({ name: 'last_name', type: 'text' })\n lastName!: string\n\n @Property({ name: 'organization_name', type: 'text' })\n organizationName!: string\n\n @Property({ type: 'text', nullable: true })\n locale?: string | null\n\n @Property({ name: 'terms_accepted', type: 'boolean', default: false })\n termsAccepted: boolean = false\n\n @Property({ name: 'marketing_consent', type: 'boolean', default: false, nullable: true })\n marketingConsent?: boolean | null = false\n\n @Property({ name: 'password_hash', type: 'text', nullable: true })\n passwordHash?: string | null\n\n @Property({ name: 'processing_started_at', type: Date, nullable: true })\n processingStartedAt?: Date | null\n\n @Property({ name: 'expires_at', type: Date })\n expiresAt!: Date\n\n @Property({ name: 'completed_at', type: Date, nullable: true })\n completedAt?: Date | null\n\n @Property({ name: 'tenant_id', type: 'uuid', nullable: true })\n tenantId?: string | null\n\n @Property({ name: 'organization_id', type: 'uuid', nullable: true })\n organizationId?: string | null\n\n @Property({ name: 'user_id', type: 'uuid', nullable: true })\n userId?: string | null\n\n @Property({ name: 'last_email_sent_at', type: Date, nullable: true })\n lastEmailSentAt?: Date | null\n\n @Property({ name: 'preparation_started_at', type: Date, nullable: true })\n preparationStartedAt?: Date | null\n\n @Property({ name: 'preparation_completed_at', type: Date, nullable: true })\n preparationCompletedAt?: Date | null\n\n @Property({ name: 'ready_email_sent_at', type: Date, nullable: true })\n readyEmailSentAt?: Date | null\n\n @Property({ name: 'created_at', type: Date, onCreate: () => new Date() })\n createdAt: Date = new Date()\n\n @Property({ name: 'updated_at', type: Date, onUpdate: () => new Date(), nullable: true })\n updatedAt?: Date\n\n @Property({ name: 'deleted_at', type: Date, nullable: true })\n deletedAt?: Date | null\n}\n\nexport type { OnboardingStatus }\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -3,6 +3,7 @@ import { jsx, jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import Image from "next/image";
|
|
4
4
|
import Link from "next/link";
|
|
5
5
|
import { useState } from "react";
|
|
6
|
+
import { useSearchParams } from "next/navigation";
|
|
6
7
|
import { useLocale, useT } from "@open-mercato/shared/lib/i18n/context";
|
|
7
8
|
import { translateWithFallback } from "@open-mercato/shared/lib/i18n/translate";
|
|
8
9
|
import { formatPasswordRequirements, getPasswordPolicy } from "@open-mercato/shared/lib/auth/passwordPolicy";
|
|
@@ -19,6 +20,7 @@ function OnboardingPageClient({ onboardingEnabled }) {
|
|
|
19
20
|
const t = useT();
|
|
20
21
|
const translate = (key, fallback, params) => translateWithFallback(t, key, fallback, params);
|
|
21
22
|
const locale = useLocale();
|
|
23
|
+
const searchParams = useSearchParams();
|
|
22
24
|
const [state, setState] = useState("idle");
|
|
23
25
|
const [globalError, setGlobalError] = useState(null);
|
|
24
26
|
const [fieldErrors, setFieldErrors] = useState({});
|
|
@@ -130,6 +132,34 @@ function OnboardingPageClient({ onboardingEnabled }) {
|
|
|
130
132
|
const onboardingDisabled = !onboardingEnabled;
|
|
131
133
|
const submitting = state === "loading";
|
|
132
134
|
const disabled = onboardingDisabled || submitting || state === "success";
|
|
135
|
+
const verifyStatusMessages = {
|
|
136
|
+
redirect_misconfigured: translate(
|
|
137
|
+
"onboarding.verifyStatus.redirectMisconfigured",
|
|
138
|
+
"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."
|
|
139
|
+
),
|
|
140
|
+
origin_not_allowed: translate(
|
|
141
|
+
"onboarding.verifyStatus.originNotAllowed",
|
|
142
|
+
"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."
|
|
143
|
+
),
|
|
144
|
+
url_not_configured: translate(
|
|
145
|
+
"onboarding.verifyStatus.urlNotConfigured",
|
|
146
|
+
"Onboarding verification is not configured. APP_URL must be set before verification links can be used."
|
|
147
|
+
),
|
|
148
|
+
already_exists: translate(
|
|
149
|
+
"onboarding.verifyStatus.alreadyExists",
|
|
150
|
+
"We already have an account with this email. Try signing in or resetting your password."
|
|
151
|
+
),
|
|
152
|
+
invalid: translate(
|
|
153
|
+
"onboarding.verifyStatus.invalid",
|
|
154
|
+
"The verification link is invalid or expired. Submit the onboarding form again to receive a new link."
|
|
155
|
+
),
|
|
156
|
+
error: translate(
|
|
157
|
+
"onboarding.verifyStatus.error",
|
|
158
|
+
"We could not complete verification. Please try again or contact support."
|
|
159
|
+
)
|
|
160
|
+
};
|
|
161
|
+
const verifyStatus = searchParams.get("status") ?? "";
|
|
162
|
+
const verifyStatusError = verifyStatusMessages[verifyStatus] ?? null;
|
|
133
163
|
return /* @__PURE__ */ jsx("div", { className: "relative flex min-h-svh items-center justify-center bg-muted/50 px-4 pb-24", children: /* @__PURE__ */ jsxs(Card, { className: "relative w-full max-w-lg overflow-hidden shadow-lg", children: [
|
|
134
164
|
onboardingDisabled ? /* @__PURE__ */ jsx("div", { className: "absolute inset-0 z-10 flex items-center justify-center rounded-lg bg-background/80 p-6 backdrop-blur-[2px]", children: /* @__PURE__ */ jsxs(
|
|
135
165
|
"div",
|
|
@@ -157,7 +187,7 @@ function OnboardingPageClient({ onboardingEnabled }) {
|
|
|
157
187
|
/* @__PURE__ */ jsx("strong", { className: "block text-sm font-medium", children: translate("onboarding.form.successTitle", "Check your inbox") }),
|
|
158
188
|
/* @__PURE__ */ jsx("p", { children: translate("onboarding.form.successBody", "We sent a verification link to {email}. Confirm it within 24 hours to activate your workspace.", { email: emailSubmitted }) })
|
|
159
189
|
] }),
|
|
160
|
-
state !== "success" && globalError && /* @__PURE__ */ jsx("div", { className: "mb-4 rounded-md border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700", role: "alert", "aria-live": "assertive", children: globalError }),
|
|
190
|
+
state !== "success" && (globalError || verifyStatusError) && /* @__PURE__ */ jsx("div", { className: "mb-4 rounded-md border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700", role: "alert", "aria-live": "assertive", children: globalError || verifyStatusError }),
|
|
161
191
|
/* @__PURE__ */ jsxs("form", { className: "grid gap-4", onSubmit, noValidate: true, children: [
|
|
162
192
|
/* @__PURE__ */ jsxs("div", { className: "grid gap-1", children: [
|
|
163
193
|
/* @__PURE__ */ jsx(Label, { htmlFor: "email", children: translate("onboarding.form.email", "Work email") }),
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/onboarding/frontend/onboarding/OnboardingPageClient.tsx"],
|
|
4
|
-
"sourcesContent": ["'use client'\n\nimport Image from 'next/image'\nimport Link from 'next/link'\nimport { useState } from 'react'\nimport { useLocale, useT } from '@open-mercato/shared/lib/i18n/context'\nimport { translateWithFallback } from '@open-mercato/shared/lib/i18n/translate'\nimport { formatPasswordRequirements, getPasswordPolicy } from '@open-mercato/shared/lib/auth/passwordPolicy'\nimport { onboardingStartSchema } from '@open-mercato/onboarding/modules/onboarding/data/validators'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@open-mercato/ui/primitives/card'\nimport { Checkbox } from '@open-mercato/ui/primitives/checkbox'\nimport { Input } from '@open-mercato/ui/primitives/input'\nimport { EmailInput } from '@open-mercato/ui/primitives/email-input'\nimport { PasswordInput } from '@open-mercato/ui/primitives/password-input'\nimport { Label } from '@open-mercato/ui/primitives/label'\n\ntype SubmissionState = 'idle' | 'loading' | 'success'\ntype FieldErrors = Partial<Record<\n 'email' | 'firstName' | 'lastName' | 'organizationName' | 'password' | 'confirmPassword' | 'termsAccepted' | 'marketingConsent',\n string\n>>\n\ntype Props = {\n onboardingEnabled: boolean\n}\n\nexport default function OnboardingPageClient({ onboardingEnabled }: Props) {\n const t = useT()\n const translate = (key: string, fallback: string, params?: Record<string, string | number>) =>\n translateWithFallback(t, key, fallback, params)\n const locale = useLocale()\n const [state, setState] = useState<SubmissionState>('idle')\n const [globalError, setGlobalError] = useState<string | null>(null)\n const [fieldErrors, setFieldErrors] = useState<FieldErrors>({})\n const [termsAccepted, setTermsAccepted] = useState(false)\n const [marketingConsent, setMarketingConsent] = useState(false)\n const [emailSubmitted, setEmailSubmitted] = useState<string | null>(null)\n const passwordPolicy = getPasswordPolicy()\n const passwordRequirements = formatPasswordRequirements(passwordPolicy, translate, 'onboarding.password.requirements')\n const passwordDescription = passwordRequirements\n ? translate(\n 'onboarding.password.requirements.help',\n 'Password requirements: {requirements}',\n { requirements: passwordRequirements },\n )\n : ''\n\n async function onSubmit(event: React.FormEvent<HTMLFormElement>) {\n event.preventDefault()\n if (!onboardingEnabled) return\n\n setState('loading')\n setGlobalError(null)\n setFieldErrors({})\n\n const form = new FormData(event.currentTarget)\n const payload = {\n email: String(form.get('email') ?? '').trim(),\n firstName: String(form.get('firstName') ?? '').trim(),\n lastName: String(form.get('lastName') ?? '').trim(),\n organizationName: String(form.get('organizationName') ?? '').trim(),\n password: String(form.get('password') ?? ''),\n confirmPassword: String(form.get('confirmPassword') ?? ''),\n termsAccepted,\n marketingConsent,\n locale,\n }\n\n const parsed = onboardingStartSchema.safeParse(payload)\n if (!parsed.success) {\n const issueMap: FieldErrors = {}\n parsed.error.issues.forEach((issue) => {\n const path = issue.path[0]\n if (!path) return\n switch (path) {\n case 'email':\n issueMap.email = translate('onboarding.errors.emailInvalid', 'Enter a valid work email.')\n break\n case 'firstName':\n issueMap.firstName = translate('onboarding.errors.firstNameRequired', 'First name is required.')\n break\n case 'lastName':\n issueMap.lastName = translate('onboarding.errors.lastNameRequired', 'Last name is required.')\n break\n case 'organizationName':\n issueMap.organizationName = translate('onboarding.errors.organizationNameRequired', 'Organization name is required.')\n break\n case 'password':\n issueMap.password = translate(\n 'onboarding.errors.passwordRequired',\n 'Password must meet the requirements: {requirements}.',\n { requirements: passwordRequirements },\n )\n break\n case 'confirmPassword':\n issueMap.confirmPassword = translate('onboarding.errors.passwordMismatch', 'Passwords must match.')\n break\n case 'termsAccepted':\n issueMap.termsAccepted = translate('onboarding.form.termsRequired', 'Please accept the terms to continue.')\n break\n default:\n break\n }\n })\n if (!issueMap.termsAccepted && !termsAccepted) {\n issueMap.termsAccepted = translate('onboarding.form.termsRequired', 'Please accept the terms to continue.')\n }\n setFieldErrors(issueMap)\n setState('idle')\n return\n }\n\n try {\n const call = await apiCall<{ ok?: boolean; error?: string; email?: string; fieldErrors?: Record<string, string> }>(\n '/api/onboarding/onboarding',\n {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ ...parsed.data, termsAccepted: true, marketingConsent }),\n },\n )\n const data = call.result ?? {}\n if (!call.ok || data.ok === false) {\n if (data.fieldErrors && typeof data.fieldErrors === 'object') {\n const mapped: FieldErrors = {}\n for (const key of Object.keys(data.fieldErrors)) {\n const value = data.fieldErrors[key]\n if (typeof value === 'string' && value.trim()) {\n mapped[key as keyof FieldErrors] = value\n }\n }\n setFieldErrors(mapped)\n }\n const message = typeof data.error === 'string' && data.error.trim()\n ? data.error\n : translate('onboarding.form.genericError', 'Something went wrong. Please try again.')\n setGlobalError(message)\n setState('idle')\n return\n }\n setEmailSubmitted(data.email ?? parsed.data.email)\n setState('success')\n } catch (err) {\n const message = err instanceof Error ? err.message : ''\n setGlobalError(message || translate('onboarding.form.genericError', 'Something went wrong. Please try again.'))\n setState('idle')\n }\n }\n\n const onboardingDisabled = !onboardingEnabled\n const submitting = state === 'loading'\n const disabled = onboardingDisabled || submitting || state === 'success'\n\n return (\n <div className=\"relative flex min-h-svh items-center justify-center bg-muted/50 px-4 pb-24\">\n <Card className=\"relative w-full max-w-lg overflow-hidden shadow-lg\">\n {onboardingDisabled ? (\n <div className=\"absolute inset-0 z-10 flex items-center justify-center rounded-lg bg-background/80 p-6 backdrop-blur-[2px]\">\n <div\n className=\"max-w-sm rounded-xl border border-border/70 bg-background/95 px-5 py-4 text-center shadow-sm\"\n role=\"alert\"\n aria-live=\"polite\"\n >\n <p className=\"text-base font-semibold text-foreground\">\n {translate('onboarding.disabled.title', 'Self-service onboarding is currently unavailable')}\n </p>\n <p className=\"mt-2 text-sm text-muted-foreground\">\n {translate(\n 'onboarding.disabled.description',\n 'Workspace creation is currently handled manually. Contact the Open Mercato team or your administrator to request access before continuing.',\n )}\n </p>\n <div className=\"mt-4 flex justify-center\">\n <Button asChild variant=\"outline\">\n <Link href=\"/login\">\n {translate('onboarding.disabled.cta', 'Go to login')}\n </Link>\n </Button>\n </div>\n </div>\n </div>\n ) : null}\n <CardHeader className=\"flex flex-col gap-4 p-10 text-center\">\n <div className=\"flex flex-col items-center gap-3\">\n <Image alt=\"Open Mercato\" src=\"/open-mercato.svg\" width={120} height={120} priority />\n <CardTitle className=\"text-2xl font-semibold\">\n {translate('onboarding.title', 'Create your Open Mercato workspace')}\n </CardTitle>\n <CardDescription>\n {translate('onboarding.subtitle', 'Tell us a bit about you and we will set everything up.')}\n </CardDescription>\n </div>\n </CardHeader>\n <CardContent className={onboardingDisabled ? 'pb-10 opacity-45' : 'pb-10'}>\n {state === 'success' && emailSubmitted && (\n <div className=\"mb-6 rounded-md border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm text-emerald-900\" role=\"status\" aria-live=\"polite\">\n <strong className=\"block text-sm font-medium\">\n {translate('onboarding.form.successTitle', 'Check your inbox')}\n </strong>\n <p>\n {translate('onboarding.form.successBody', 'We sent a verification link to {email}. Confirm it within 24 hours to activate your workspace.', { email: emailSubmitted })}\n </p>\n </div>\n )}\n {state !== 'success' && globalError && (\n <div className=\"mb-4 rounded-md border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700\" role=\"alert\" aria-live=\"assertive\">\n {globalError}\n </div>\n )}\n <form className=\"grid gap-4\" onSubmit={onSubmit} noValidate>\n <div className=\"grid gap-1\">\n <Label htmlFor=\"email\">{translate('onboarding.form.email', 'Work email')}</Label>\n <EmailInput\n id=\"email\"\n name=\"email\"\n required\n disabled={disabled}\n aria-invalid={Boolean(fieldErrors.email)}\n aria-describedby={fieldErrors.email ? 'email-error' : undefined}\n className={fieldErrors.email ? 'border-red-500 aria-invalid:ring-destructive' : undefined}\n />\n {fieldErrors.email && (\n <p id=\"email-error\" className=\"text-xs text-red-600\">{fieldErrors.email}</p>\n )}\n </div>\n <div className=\"grid gap-1 sm:grid-cols-2 sm:gap-4\">\n <div className=\"grid gap-1\">\n <Label htmlFor=\"firstName\">{translate('onboarding.form.firstName', 'First name')}</Label>\n <Input\n id=\"firstName\"\n name=\"firstName\"\n type=\"text\"\n required\n disabled={disabled}\n autoComplete=\"given-name\"\n aria-invalid={Boolean(fieldErrors.firstName)}\n aria-describedby={fieldErrors.firstName ? 'firstName-error' : undefined}\n className={fieldErrors.firstName ? 'border-red-500 aria-invalid:ring-destructive' : undefined}\n />\n {fieldErrors.firstName && (\n <p id=\"firstName-error\" className=\"text-xs text-red-600\">{fieldErrors.firstName}</p>\n )}\n </div>\n <div className=\"grid gap-1\">\n <Label htmlFor=\"lastName\">{translate('onboarding.form.lastName', 'Last name')}</Label>\n <Input\n id=\"lastName\"\n name=\"lastName\"\n type=\"text\"\n required\n disabled={disabled}\n autoComplete=\"family-name\"\n aria-invalid={Boolean(fieldErrors.lastName)}\n aria-describedby={fieldErrors.lastName ? 'lastName-error' : undefined}\n className={fieldErrors.lastName ? 'border-red-500 aria-invalid:ring-destructive' : undefined}\n />\n {fieldErrors.lastName && (\n <p id=\"lastName-error\" className=\"text-xs text-red-600\">{fieldErrors.lastName}</p>\n )}\n </div>\n </div>\n <div className=\"grid gap-1\">\n <Label htmlFor=\"organizationName\">{translate('onboarding.form.organizationName', 'Organization name')}</Label>\n <Input\n id=\"organizationName\"\n name=\"organizationName\"\n type=\"text\"\n required\n disabled={disabled}\n autoComplete=\"organization\"\n aria-invalid={Boolean(fieldErrors.organizationName)}\n aria-describedby={fieldErrors.organizationName ? 'organizationName-error' : undefined}\n className={fieldErrors.organizationName ? 'border-red-500 aria-invalid:ring-destructive' : undefined}\n />\n {fieldErrors.organizationName && (\n <p id=\"organizationName-error\" className=\"text-xs text-red-600\">{fieldErrors.organizationName}</p>\n )}\n </div>\n <div className=\"grid gap-1\">\n <Label htmlFor=\"password\">{translate('onboarding.form.password', 'Password')}</Label>\n <PasswordInput\n id=\"password\"\n name=\"password\"\n required\n disabled={disabled}\n autoComplete=\"new-password\"\n minLength={passwordPolicy.minLength}\n aria-invalid={Boolean(fieldErrors.password)}\n aria-describedby={fieldErrors.password ? 'password-error' : undefined}\n className={fieldErrors.password ? 'border-red-500 aria-invalid:ring-destructive' : undefined}\n />\n {passwordDescription ? (\n <p className=\"text-xs text-muted-foreground\">{passwordDescription}</p>\n ) : null}\n {fieldErrors.password && (\n <p id=\"password-error\" className=\"text-xs text-red-600\">{fieldErrors.password}</p>\n )}\n </div>\n <div className=\"grid gap-1\">\n <Label htmlFor=\"confirmPassword\">{translate('onboarding.form.confirmPassword', 'Confirm password')}</Label>\n <PasswordInput\n id=\"confirmPassword\"\n name=\"confirmPassword\"\n required\n disabled={disabled}\n autoComplete=\"new-password\"\n aria-invalid={Boolean(fieldErrors.confirmPassword)}\n aria-describedby={fieldErrors.confirmPassword ? 'confirmPassword-error' : undefined}\n className={fieldErrors.confirmPassword ? 'border-red-500 aria-invalid:ring-destructive' : undefined}\n />\n {fieldErrors.confirmPassword && (\n <p id=\"confirmPassword-error\" className=\"text-xs text-red-600\">{fieldErrors.confirmPassword}</p>\n )}\n </div>\n <label className=\"flex items-start gap-3 text-sm text-muted-foreground\">\n <Checkbox\n id=\"terms\"\n checked={termsAccepted}\n disabled={disabled}\n onCheckedChange={(value: boolean | 'indeterminate') => {\n setTermsAccepted(value === true)\n if (value === true) {\n setFieldErrors((prev) => {\n const next = { ...prev }\n delete next.termsAccepted\n return next\n })\n }\n }}\n aria-invalid={Boolean(fieldErrors.termsAccepted)}\n />\n <span>\n {translate('onboarding.form.termsLabel', 'I have read and accept the ')}\n <a className=\"underline hover:text-foreground\" href=\"/terms\" target=\"_blank\" rel=\"noreferrer\">\n {translate('onboarding.form.termsLink', 'Terms of Service')}\n </a>\n {translate('onboarding.form.termsAnd', ' and ')}\n <a className=\"underline hover:text-foreground\" href=\"/privacy\" target=\"_blank\" rel=\"noreferrer\">\n {translate('onboarding.form.privacyLink', 'Privacy Policy')}\n </a>\n {fieldErrors.termsAccepted && (\n <span className=\"mt-1 block text-xs text-red-600\">{fieldErrors.termsAccepted}</span>\n )}\n </span>\n </label>\n <label className=\"flex items-start gap-3 text-sm text-muted-foreground\">\n <Checkbox\n id=\"marketingConsent\"\n checked={marketingConsent}\n disabled={disabled}\n onCheckedChange={(value: boolean | 'indeterminate') => {\n setMarketingConsent(value === true)\n }}\n />\n <span>\n {translate(\n 'onboarding.form.marketingLabel',\n \"I consent to receiving direct marketing from CT Tornado by email at the address I provide. I'm aware that I can withdraw my consent at any time (e.g., via the unsubscribe link), and that CT Tornado may also process my email address under its legitimate interests to manage its mailing list, keep proof of my consent, and record opt-outs. See our {termsLink} and {privacyLink}.\",\n )\n .split(/{termsLink}|{privacyLink}/)\n .map((part, index, parts) => (\n <span key={index}>\n {part}\n {index < parts.length - 1 && (\n index === 0 ? (\n <a className=\"underline hover:text-foreground\" href=\"/terms\" target=\"_blank\" rel=\"noreferrer\">\n {translate('onboarding.form.termsLink', 'Terms of Service')}\n </a>\n ) : (\n <a className=\"underline hover:text-foreground\" href=\"/privacy\" target=\"_blank\" rel=\"noreferrer\">\n {translate('onboarding.form.privacyLink', 'Privacy Policy')}\n </a>\n )\n )}\n </span>\n ))}\n </span>\n </label>\n <Button\n type=\"submit\"\n disabled={disabled}\n className=\"mt-2 h-11 bg-foreground text-background transition hover:opacity-80 disabled:cursor-not-allowed disabled:opacity-50\"\n >\n {submitting\n ? translate('onboarding.form.loading', 'Sending...')\n : translate('onboarding.form.submit', 'Send verification email')}\n </Button>\n </form>\n </CardContent>\n </Card>\n </div>\n )\n}\n"],
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["'use client'\n\nimport Image from 'next/image'\nimport Link from 'next/link'\nimport { useState } from 'react'\nimport { useSearchParams } from 'next/navigation'\nimport { useLocale, useT } from '@open-mercato/shared/lib/i18n/context'\nimport { translateWithFallback } from '@open-mercato/shared/lib/i18n/translate'\nimport { formatPasswordRequirements, getPasswordPolicy } from '@open-mercato/shared/lib/auth/passwordPolicy'\nimport { onboardingStartSchema } from '@open-mercato/onboarding/modules/onboarding/data/validators'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@open-mercato/ui/primitives/card'\nimport { Checkbox } from '@open-mercato/ui/primitives/checkbox'\nimport { Input } from '@open-mercato/ui/primitives/input'\nimport { EmailInput } from '@open-mercato/ui/primitives/email-input'\nimport { PasswordInput } from '@open-mercato/ui/primitives/password-input'\nimport { Label } from '@open-mercato/ui/primitives/label'\n\ntype SubmissionState = 'idle' | 'loading' | 'success'\ntype FieldErrors = Partial<Record<\n 'email' | 'firstName' | 'lastName' | 'organizationName' | 'password' | 'confirmPassword' | 'termsAccepted' | 'marketingConsent',\n string\n>>\n\ntype Props = {\n onboardingEnabled: boolean\n}\n\nexport default function OnboardingPageClient({ onboardingEnabled }: Props) {\n const t = useT()\n const translate = (key: string, fallback: string, params?: Record<string, string | number>) =>\n translateWithFallback(t, key, fallback, params)\n const locale = useLocale()\n const searchParams = useSearchParams()\n const [state, setState] = useState<SubmissionState>('idle')\n const [globalError, setGlobalError] = useState<string | null>(null)\n const [fieldErrors, setFieldErrors] = useState<FieldErrors>({})\n const [termsAccepted, setTermsAccepted] = useState(false)\n const [marketingConsent, setMarketingConsent] = useState(false)\n const [emailSubmitted, setEmailSubmitted] = useState<string | null>(null)\n const passwordPolicy = getPasswordPolicy()\n const passwordRequirements = formatPasswordRequirements(passwordPolicy, translate, 'onboarding.password.requirements')\n const passwordDescription = passwordRequirements\n ? translate(\n 'onboarding.password.requirements.help',\n 'Password requirements: {requirements}',\n { requirements: passwordRequirements },\n )\n : ''\n\n async function onSubmit(event: React.FormEvent<HTMLFormElement>) {\n event.preventDefault()\n if (!onboardingEnabled) return\n\n setState('loading')\n setGlobalError(null)\n setFieldErrors({})\n\n const form = new FormData(event.currentTarget)\n const payload = {\n email: String(form.get('email') ?? '').trim(),\n firstName: String(form.get('firstName') ?? '').trim(),\n lastName: String(form.get('lastName') ?? '').trim(),\n organizationName: String(form.get('organizationName') ?? '').trim(),\n password: String(form.get('password') ?? ''),\n confirmPassword: String(form.get('confirmPassword') ?? ''),\n termsAccepted,\n marketingConsent,\n locale,\n }\n\n const parsed = onboardingStartSchema.safeParse(payload)\n if (!parsed.success) {\n const issueMap: FieldErrors = {}\n parsed.error.issues.forEach((issue) => {\n const path = issue.path[0]\n if (!path) return\n switch (path) {\n case 'email':\n issueMap.email = translate('onboarding.errors.emailInvalid', 'Enter a valid work email.')\n break\n case 'firstName':\n issueMap.firstName = translate('onboarding.errors.firstNameRequired', 'First name is required.')\n break\n case 'lastName':\n issueMap.lastName = translate('onboarding.errors.lastNameRequired', 'Last name is required.')\n break\n case 'organizationName':\n issueMap.organizationName = translate('onboarding.errors.organizationNameRequired', 'Organization name is required.')\n break\n case 'password':\n issueMap.password = translate(\n 'onboarding.errors.passwordRequired',\n 'Password must meet the requirements: {requirements}.',\n { requirements: passwordRequirements },\n )\n break\n case 'confirmPassword':\n issueMap.confirmPassword = translate('onboarding.errors.passwordMismatch', 'Passwords must match.')\n break\n case 'termsAccepted':\n issueMap.termsAccepted = translate('onboarding.form.termsRequired', 'Please accept the terms to continue.')\n break\n default:\n break\n }\n })\n if (!issueMap.termsAccepted && !termsAccepted) {\n issueMap.termsAccepted = translate('onboarding.form.termsRequired', 'Please accept the terms to continue.')\n }\n setFieldErrors(issueMap)\n setState('idle')\n return\n }\n\n try {\n const call = await apiCall<{ ok?: boolean; error?: string; email?: string; fieldErrors?: Record<string, string> }>(\n '/api/onboarding/onboarding',\n {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ ...parsed.data, termsAccepted: true, marketingConsent }),\n },\n )\n const data = call.result ?? {}\n if (!call.ok || data.ok === false) {\n if (data.fieldErrors && typeof data.fieldErrors === 'object') {\n const mapped: FieldErrors = {}\n for (const key of Object.keys(data.fieldErrors)) {\n const value = data.fieldErrors[key]\n if (typeof value === 'string' && value.trim()) {\n mapped[key as keyof FieldErrors] = value\n }\n }\n setFieldErrors(mapped)\n }\n const message = typeof data.error === 'string' && data.error.trim()\n ? data.error\n : translate('onboarding.form.genericError', 'Something went wrong. Please try again.')\n setGlobalError(message)\n setState('idle')\n return\n }\n setEmailSubmitted(data.email ?? parsed.data.email)\n setState('success')\n } catch (err) {\n const message = err instanceof Error ? err.message : ''\n setGlobalError(message || translate('onboarding.form.genericError', 'Something went wrong. Please try again.'))\n setState('idle')\n }\n }\n\n const onboardingDisabled = !onboardingEnabled\n const submitting = state === 'loading'\n const disabled = onboardingDisabled || submitting || state === 'success'\n const verifyStatusMessages: Record<string, string> = {\n redirect_misconfigured: translate(\n 'onboarding.verifyStatus.redirectMisconfigured',\n '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.',\n ),\n origin_not_allowed: translate(\n 'onboarding.verifyStatus.originNotAllowed',\n '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.',\n ),\n url_not_configured: translate(\n 'onboarding.verifyStatus.urlNotConfigured',\n 'Onboarding verification is not configured. APP_URL must be set before verification links can be used.',\n ),\n already_exists: translate(\n 'onboarding.verifyStatus.alreadyExists',\n 'We already have an account with this email. Try signing in or resetting your password.',\n ),\n invalid: translate(\n 'onboarding.verifyStatus.invalid',\n 'The verification link is invalid or expired. Submit the onboarding form again to receive a new link.',\n ),\n error: translate(\n 'onboarding.verifyStatus.error',\n 'We could not complete verification. Please try again or contact support.',\n ),\n }\n const verifyStatus = searchParams.get('status') ?? ''\n const verifyStatusError = verifyStatusMessages[verifyStatus] ?? null\n\n return (\n <div className=\"relative flex min-h-svh items-center justify-center bg-muted/50 px-4 pb-24\">\n <Card className=\"relative w-full max-w-lg overflow-hidden shadow-lg\">\n {onboardingDisabled ? (\n <div className=\"absolute inset-0 z-10 flex items-center justify-center rounded-lg bg-background/80 p-6 backdrop-blur-[2px]\">\n <div\n className=\"max-w-sm rounded-xl border border-border/70 bg-background/95 px-5 py-4 text-center shadow-sm\"\n role=\"alert\"\n aria-live=\"polite\"\n >\n <p className=\"text-base font-semibold text-foreground\">\n {translate('onboarding.disabled.title', 'Self-service onboarding is currently unavailable')}\n </p>\n <p className=\"mt-2 text-sm text-muted-foreground\">\n {translate(\n 'onboarding.disabled.description',\n 'Workspace creation is currently handled manually. Contact the Open Mercato team or your administrator to request access before continuing.',\n )}\n </p>\n <div className=\"mt-4 flex justify-center\">\n <Button asChild variant=\"outline\">\n <Link href=\"/login\">\n {translate('onboarding.disabled.cta', 'Go to login')}\n </Link>\n </Button>\n </div>\n </div>\n </div>\n ) : null}\n <CardHeader className=\"flex flex-col gap-4 p-10 text-center\">\n <div className=\"flex flex-col items-center gap-3\">\n <Image alt=\"Open Mercato\" src=\"/open-mercato.svg\" width={120} height={120} priority />\n <CardTitle className=\"text-2xl font-semibold\">\n {translate('onboarding.title', 'Create your Open Mercato workspace')}\n </CardTitle>\n <CardDescription>\n {translate('onboarding.subtitle', 'Tell us a bit about you and we will set everything up.')}\n </CardDescription>\n </div>\n </CardHeader>\n <CardContent className={onboardingDisabled ? 'pb-10 opacity-45' : 'pb-10'}>\n {state === 'success' && emailSubmitted && (\n <div className=\"mb-6 rounded-md border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm text-emerald-900\" role=\"status\" aria-live=\"polite\">\n <strong className=\"block text-sm font-medium\">\n {translate('onboarding.form.successTitle', 'Check your inbox')}\n </strong>\n <p>\n {translate('onboarding.form.successBody', 'We sent a verification link to {email}. Confirm it within 24 hours to activate your workspace.', { email: emailSubmitted })}\n </p>\n </div>\n )}\n {state !== 'success' && (globalError || verifyStatusError) && (\n <div className=\"mb-4 rounded-md border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700\" role=\"alert\" aria-live=\"assertive\">\n {globalError || verifyStatusError}\n </div>\n )}\n <form className=\"grid gap-4\" onSubmit={onSubmit} noValidate>\n <div className=\"grid gap-1\">\n <Label htmlFor=\"email\">{translate('onboarding.form.email', 'Work email')}</Label>\n <EmailInput\n id=\"email\"\n name=\"email\"\n required\n disabled={disabled}\n aria-invalid={Boolean(fieldErrors.email)}\n aria-describedby={fieldErrors.email ? 'email-error' : undefined}\n className={fieldErrors.email ? 'border-red-500 aria-invalid:ring-destructive' : undefined}\n />\n {fieldErrors.email && (\n <p id=\"email-error\" className=\"text-xs text-red-600\">{fieldErrors.email}</p>\n )}\n </div>\n <div className=\"grid gap-1 sm:grid-cols-2 sm:gap-4\">\n <div className=\"grid gap-1\">\n <Label htmlFor=\"firstName\">{translate('onboarding.form.firstName', 'First name')}</Label>\n <Input\n id=\"firstName\"\n name=\"firstName\"\n type=\"text\"\n required\n disabled={disabled}\n autoComplete=\"given-name\"\n aria-invalid={Boolean(fieldErrors.firstName)}\n aria-describedby={fieldErrors.firstName ? 'firstName-error' : undefined}\n className={fieldErrors.firstName ? 'border-red-500 aria-invalid:ring-destructive' : undefined}\n />\n {fieldErrors.firstName && (\n <p id=\"firstName-error\" className=\"text-xs text-red-600\">{fieldErrors.firstName}</p>\n )}\n </div>\n <div className=\"grid gap-1\">\n <Label htmlFor=\"lastName\">{translate('onboarding.form.lastName', 'Last name')}</Label>\n <Input\n id=\"lastName\"\n name=\"lastName\"\n type=\"text\"\n required\n disabled={disabled}\n autoComplete=\"family-name\"\n aria-invalid={Boolean(fieldErrors.lastName)}\n aria-describedby={fieldErrors.lastName ? 'lastName-error' : undefined}\n className={fieldErrors.lastName ? 'border-red-500 aria-invalid:ring-destructive' : undefined}\n />\n {fieldErrors.lastName && (\n <p id=\"lastName-error\" className=\"text-xs text-red-600\">{fieldErrors.lastName}</p>\n )}\n </div>\n </div>\n <div className=\"grid gap-1\">\n <Label htmlFor=\"organizationName\">{translate('onboarding.form.organizationName', 'Organization name')}</Label>\n <Input\n id=\"organizationName\"\n name=\"organizationName\"\n type=\"text\"\n required\n disabled={disabled}\n autoComplete=\"organization\"\n aria-invalid={Boolean(fieldErrors.organizationName)}\n aria-describedby={fieldErrors.organizationName ? 'organizationName-error' : undefined}\n className={fieldErrors.organizationName ? 'border-red-500 aria-invalid:ring-destructive' : undefined}\n />\n {fieldErrors.organizationName && (\n <p id=\"organizationName-error\" className=\"text-xs text-red-600\">{fieldErrors.organizationName}</p>\n )}\n </div>\n <div className=\"grid gap-1\">\n <Label htmlFor=\"password\">{translate('onboarding.form.password', 'Password')}</Label>\n <PasswordInput\n id=\"password\"\n name=\"password\"\n required\n disabled={disabled}\n autoComplete=\"new-password\"\n minLength={passwordPolicy.minLength}\n aria-invalid={Boolean(fieldErrors.password)}\n aria-describedby={fieldErrors.password ? 'password-error' : undefined}\n className={fieldErrors.password ? 'border-red-500 aria-invalid:ring-destructive' : undefined}\n />\n {passwordDescription ? (\n <p className=\"text-xs text-muted-foreground\">{passwordDescription}</p>\n ) : null}\n {fieldErrors.password && (\n <p id=\"password-error\" className=\"text-xs text-red-600\">{fieldErrors.password}</p>\n )}\n </div>\n <div className=\"grid gap-1\">\n <Label htmlFor=\"confirmPassword\">{translate('onboarding.form.confirmPassword', 'Confirm password')}</Label>\n <PasswordInput\n id=\"confirmPassword\"\n name=\"confirmPassword\"\n required\n disabled={disabled}\n autoComplete=\"new-password\"\n aria-invalid={Boolean(fieldErrors.confirmPassword)}\n aria-describedby={fieldErrors.confirmPassword ? 'confirmPassword-error' : undefined}\n className={fieldErrors.confirmPassword ? 'border-red-500 aria-invalid:ring-destructive' : undefined}\n />\n {fieldErrors.confirmPassword && (\n <p id=\"confirmPassword-error\" className=\"text-xs text-red-600\">{fieldErrors.confirmPassword}</p>\n )}\n </div>\n <label className=\"flex items-start gap-3 text-sm text-muted-foreground\">\n <Checkbox\n id=\"terms\"\n checked={termsAccepted}\n disabled={disabled}\n onCheckedChange={(value: boolean | 'indeterminate') => {\n setTermsAccepted(value === true)\n if (value === true) {\n setFieldErrors((prev) => {\n const next = { ...prev }\n delete next.termsAccepted\n return next\n })\n }\n }}\n aria-invalid={Boolean(fieldErrors.termsAccepted)}\n />\n <span>\n {translate('onboarding.form.termsLabel', 'I have read and accept the ')}\n <a className=\"underline hover:text-foreground\" href=\"/terms\" target=\"_blank\" rel=\"noreferrer\">\n {translate('onboarding.form.termsLink', 'Terms of Service')}\n </a>\n {translate('onboarding.form.termsAnd', ' and ')}\n <a className=\"underline hover:text-foreground\" href=\"/privacy\" target=\"_blank\" rel=\"noreferrer\">\n {translate('onboarding.form.privacyLink', 'Privacy Policy')}\n </a>\n {fieldErrors.termsAccepted && (\n <span className=\"mt-1 block text-xs text-red-600\">{fieldErrors.termsAccepted}</span>\n )}\n </span>\n </label>\n <label className=\"flex items-start gap-3 text-sm text-muted-foreground\">\n <Checkbox\n id=\"marketingConsent\"\n checked={marketingConsent}\n disabled={disabled}\n onCheckedChange={(value: boolean | 'indeterminate') => {\n setMarketingConsent(value === true)\n }}\n />\n <span>\n {translate(\n 'onboarding.form.marketingLabel',\n \"I consent to receiving direct marketing from CT Tornado by email at the address I provide. I'm aware that I can withdraw my consent at any time (e.g., via the unsubscribe link), and that CT Tornado may also process my email address under its legitimate interests to manage its mailing list, keep proof of my consent, and record opt-outs. See our {termsLink} and {privacyLink}.\",\n )\n .split(/{termsLink}|{privacyLink}/)\n .map((part, index, parts) => (\n <span key={index}>\n {part}\n {index < parts.length - 1 && (\n index === 0 ? (\n <a className=\"underline hover:text-foreground\" href=\"/terms\" target=\"_blank\" rel=\"noreferrer\">\n {translate('onboarding.form.termsLink', 'Terms of Service')}\n </a>\n ) : (\n <a className=\"underline hover:text-foreground\" href=\"/privacy\" target=\"_blank\" rel=\"noreferrer\">\n {translate('onboarding.form.privacyLink', 'Privacy Policy')}\n </a>\n )\n )}\n </span>\n ))}\n </span>\n </label>\n <Button\n type=\"submit\"\n disabled={disabled}\n className=\"mt-2 h-11 bg-foreground text-background transition hover:opacity-80 disabled:cursor-not-allowed disabled:opacity-50\"\n >\n {submitting\n ? translate('onboarding.form.loading', 'Sending...')\n : translate('onboarding.form.submit', 'Send verification email')}\n </Button>\n </form>\n </CardContent>\n </Card>\n </div>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -18,6 +18,7 @@ function PreparingPageClient() {
|
|
|
18
18
|
const tenantId = (searchParams.get("tenant") || "").trim();
|
|
19
19
|
const [tenantName, setTenantName] = useState(null);
|
|
20
20
|
const [redirecting, setRedirecting] = useState(false);
|
|
21
|
+
const [statusError, setStatusError] = useState(null);
|
|
21
22
|
useEffect(() => {
|
|
22
23
|
if (!tenantId) {
|
|
23
24
|
setTenantName(null);
|
|
@@ -41,10 +42,28 @@ function PreparingPageClient() {
|
|
|
41
42
|
if (!tenantId) return;
|
|
42
43
|
let active = true;
|
|
43
44
|
let timeoutId = null;
|
|
44
|
-
const
|
|
45
|
+
const genericStatusError = translate(
|
|
46
|
+
"onboarding.preparing.statusErrorBody",
|
|
47
|
+
"We could not check the workspace status. The workspace may still be preparing; this page will keep retrying."
|
|
48
|
+
);
|
|
49
|
+
function schedulePoll() {
|
|
50
|
+
if (!active) return;
|
|
51
|
+
timeoutId = setTimeout(() => {
|
|
52
|
+
void poll();
|
|
53
|
+
}, 3e3);
|
|
54
|
+
}
|
|
55
|
+
async function poll() {
|
|
45
56
|
try {
|
|
46
57
|
const { result } = await apiCall(`/api/onboarding/onboarding/status?tenantId=${encodeURIComponent(tenantId)}`);
|
|
47
|
-
if (!active
|
|
58
|
+
if (!active) return;
|
|
59
|
+
if (!result?.ok) {
|
|
60
|
+
setStatusError(
|
|
61
|
+
typeof result?.error === "string" && result.error.trim() ? result.error : genericStatusError
|
|
62
|
+
);
|
|
63
|
+
schedulePoll();
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
setStatusError(null);
|
|
48
67
|
if (result.ready && result.loginUrl) {
|
|
49
68
|
setRedirecting(true);
|
|
50
69
|
router.replace(result.loginUrl);
|
|
@@ -52,12 +71,10 @@ function PreparingPageClient() {
|
|
|
52
71
|
}
|
|
53
72
|
} catch {
|
|
54
73
|
if (!active) return;
|
|
74
|
+
setStatusError(genericStatusError);
|
|
55
75
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
void poll();
|
|
59
|
-
}, 3e3);
|
|
60
|
-
};
|
|
76
|
+
schedulePoll();
|
|
77
|
+
}
|
|
61
78
|
void poll();
|
|
62
79
|
return () => {
|
|
63
80
|
active = false;
|
|
@@ -86,6 +103,18 @@ function PreparingPageClient() {
|
|
|
86
103
|
"onboarding.preparing.emailNotice",
|
|
87
104
|
"You do not need to keep this page open. We will email you when everything is ready."
|
|
88
105
|
) }),
|
|
106
|
+
statusError ? /* @__PURE__ */ jsxs(
|
|
107
|
+
"div",
|
|
108
|
+
{
|
|
109
|
+
className: "rounded-md border border-red-200 bg-red-50 px-4 py-3 text-left text-sm text-red-700",
|
|
110
|
+
role: "alert",
|
|
111
|
+
"aria-live": "assertive",
|
|
112
|
+
children: [
|
|
113
|
+
/* @__PURE__ */ jsx("strong", { className: "block text-sm font-medium", children: translate("onboarding.preparing.statusErrorTitle", "Workspace status check failed") }),
|
|
114
|
+
/* @__PURE__ */ jsx("p", { className: "mt-1", children: statusError })
|
|
115
|
+
]
|
|
116
|
+
}
|
|
117
|
+
) : null,
|
|
89
118
|
/* @__PURE__ */ jsx("div", { className: "flex flex-col items-center justify-center gap-3 sm:flex-row", children: /* @__PURE__ */ jsx(Button, { asChild: true, children: /* @__PURE__ */ jsx(Link, { href: "/", children: translate("onboarding.preparing.homeCta", "Go to home page") }) }) })
|
|
90
119
|
] })
|
|
91
120
|
] }) });
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/onboarding/frontend/onboarding/preparing/PreparingPageClient.tsx"],
|
|
4
|
-
"sourcesContent": ["'use client'\n\nimport Image from 'next/image'\nimport Link from 'next/link'\nimport { useEffect, useState } from 'react'\nimport { useRouter, useSearchParams } from 'next/navigation'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { translateWithFallback } from '@open-mercato/shared/lib/i18n/translate'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@open-mercato/ui/primitives/card'\nimport { Spinner } from '@open-mercato/ui/primitives/spinner'\n\nexport default function PreparingPageClient() {\n const t = useT()\n const router = useRouter()\n const translate = (key: string, fallback: string, params?: Record<string, string | number>) =>\n translateWithFallback(t, key, fallback, params)\n const searchParams = useSearchParams()\n const tenantId = (searchParams.get('tenant') || '').trim()\n const [tenantName, setTenantName] = useState<string | null>(null)\n const [redirecting, setRedirecting] = useState(false)\n\n useEffect(() => {\n if (!tenantId) {\n setTenantName(null)\n return\n }\n let active = true\n apiCall<{ ok?: boolean; tenant?: { id: string; name: string } }>(\n `/api/directory/tenants/lookup?tenantId=${encodeURIComponent(tenantId)}`,\n )\n .then(({ result }) => {\n if (!active) return\n setTenantName(result?.ok && result.tenant ? result.tenant.name : null)\n })\n .catch(() => {\n if (!active) return\n setTenantName(null)\n })\n return () => {\n active = false\n }\n }, [tenantId])\n\n useEffect(() => {\n if (!tenantId) return\n let active = true\n let timeoutId: ReturnType<typeof setTimeout> | null = null\n
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["'use client'\n\nimport Image from 'next/image'\nimport Link from 'next/link'\nimport { useEffect, useState } from 'react'\nimport { useRouter, useSearchParams } from 'next/navigation'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { translateWithFallback } from '@open-mercato/shared/lib/i18n/translate'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@open-mercato/ui/primitives/card'\nimport { Spinner } from '@open-mercato/ui/primitives/spinner'\n\nexport default function PreparingPageClient() {\n const t = useT()\n const router = useRouter()\n const translate = (key: string, fallback: string, params?: Record<string, string | number>) =>\n translateWithFallback(t, key, fallback, params)\n const searchParams = useSearchParams()\n const tenantId = (searchParams.get('tenant') || '').trim()\n const [tenantName, setTenantName] = useState<string | null>(null)\n const [redirecting, setRedirecting] = useState(false)\n const [statusError, setStatusError] = useState<string | null>(null)\n\n useEffect(() => {\n if (!tenantId) {\n setTenantName(null)\n return\n }\n let active = true\n apiCall<{ ok?: boolean; tenant?: { id: string; name: string } }>(\n `/api/directory/tenants/lookup?tenantId=${encodeURIComponent(tenantId)}`,\n )\n .then(({ result }) => {\n if (!active) return\n setTenantName(result?.ok && result.tenant ? result.tenant.name : null)\n })\n .catch(() => {\n if (!active) return\n setTenantName(null)\n })\n return () => {\n active = false\n }\n }, [tenantId])\n\n useEffect(() => {\n if (!tenantId) return\n let active = true\n let timeoutId: ReturnType<typeof setTimeout> | null = null\n const genericStatusError = translate(\n 'onboarding.preparing.statusErrorBody',\n 'We could not check the workspace status. The workspace may still be preparing; this page will keep retrying.',\n )\n function schedulePoll() {\n if (!active) return\n timeoutId = setTimeout(() => {\n void poll()\n }, 3000)\n }\n\n async function poll() {\n try {\n const { result } = await apiCall<{\n ok?: boolean\n ready?: boolean\n loginUrl?: string | null\n error?: string\n }>(`/api/onboarding/onboarding/status?tenantId=${encodeURIComponent(tenantId)}`)\n if (!active) return\n if (!result?.ok) {\n setStatusError(\n typeof result?.error === 'string' && result.error.trim()\n ? result.error\n : genericStatusError,\n )\n schedulePoll()\n return\n }\n setStatusError(null)\n if (result.ready && result.loginUrl) {\n setRedirecting(true)\n router.replace(result.loginUrl)\n return\n }\n } catch {\n if (!active) return\n setStatusError(genericStatusError)\n }\n schedulePoll()\n }\n\n void poll()\n\n return () => {\n active = false\n if (timeoutId) clearTimeout(timeoutId)\n }\n }, [router, tenantId])\n\n return (\n <div className=\"relative flex min-h-svh items-center justify-center bg-muted/50 px-4 pb-24\">\n <Card className=\"relative w-full max-w-lg overflow-hidden shadow-lg\">\n <CardHeader className=\"flex flex-col gap-4 p-10 text-center\">\n <div className=\"flex flex-col items-center gap-4\">\n <Image alt=\"Open Mercato\" src=\"/open-mercato.svg\" width={120} height={120} priority />\n <span className=\"inline-flex h-16 w-16 items-center justify-center rounded-full bg-foreground/5\">\n <Spinner size=\"lg\" />\n </span>\n <CardTitle className=\"text-2xl font-semibold\">\n {translate('onboarding.preparing.title', 'We are preparing your workspace')}\n </CardTitle>\n <CardDescription className=\"max-w-md text-balance\">\n {tenantName\n ? translate(\n 'onboarding.preparing.descriptionWithTenant',\n '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.',\n { tenant: tenantName },\n )\n : translate(\n 'onboarding.preparing.description',\n 'We are finishing your demo environment. We will send you an email with the correct tenant login link as soon as it is ready.',\n )}\n </CardDescription>\n </div>\n </CardHeader>\n <CardContent className=\"space-y-6 pb-10 text-center\">\n <div className=\"rounded-xl border border-border/70 bg-background/80 px-4 py-4 text-sm text-muted-foreground\">\n {redirecting\n ? translate(\n 'onboarding.preparing.redirecting',\n 'Your workspace is ready. Redirecting you to the tenant login page now.',\n )\n : translate(\n 'onboarding.preparing.emailNotice',\n 'You do not need to keep this page open. We will email you when everything is ready.',\n )}\n </div>\n {statusError ? (\n <div\n className=\"rounded-md border border-red-200 bg-red-50 px-4 py-3 text-left text-sm text-red-700\"\n role=\"alert\"\n aria-live=\"assertive\"\n >\n <strong className=\"block text-sm font-medium\">\n {translate('onboarding.preparing.statusErrorTitle', 'Workspace status check failed')}\n </strong>\n <p className=\"mt-1\">{statusError}</p>\n </div>\n ) : null}\n <div className=\"flex flex-col items-center justify-center gap-3 sm:flex-row\">\n <Button asChild>\n <Link href=\"/\">\n {translate('onboarding.preparing.homeCta', 'Go to home page')}\n </Link>\n </Button>\n </div>\n </CardContent>\n </Card>\n </div>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
|
|
2
|
+
import { flattenSystemEntityIds } from "@open-mercato/shared/lib/entities/system-entities";
|
|
3
|
+
import { getEntityIds } from "@open-mercato/shared/lib/encryption/entityIds";
|
|
4
|
+
import { getModules } from "@open-mercato/shared/lib/modules/registry";
|
|
5
|
+
import { OnboardingService } from "@open-mercato/onboarding/modules/onboarding/lib/service";
|
|
6
|
+
import { sendWorkspaceReadyEmail } from "@open-mercato/onboarding/modules/onboarding/lib/ready-email";
|
|
7
|
+
import { reindexEntity } from "@open-mercato/core/modules/query_index/lib/reindexer";
|
|
8
|
+
import { purgeIndexScope } from "@open-mercato/core/modules/query_index/lib/purge";
|
|
9
|
+
import { refreshCoverageSnapshot } from "@open-mercato/core/modules/query_index/lib/coverage";
|
|
10
|
+
import { isUniqueViolation } from "@open-mercato/shared/lib/crud/errors";
|
|
11
|
+
import { PREPARATION_CLAIM_STALE_MS } from "@open-mercato/onboarding/modules/onboarding/lib/preparation-claim";
|
|
12
|
+
const VECTOR_REINDEX_ENQUEUE_TIMEOUT_MS = 5e3;
|
|
13
|
+
const SEED_EXAMPLES_TIMEOUT_MS = 15e3;
|
|
14
|
+
function resolveProvisioningIds(request) {
|
|
15
|
+
if (!request.tenantId || !request.organizationId || !request.userId) return null;
|
|
16
|
+
return {
|
|
17
|
+
tenantId: request.tenantId,
|
|
18
|
+
organizationId: request.organizationId,
|
|
19
|
+
userId: request.userId
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
function createTimeoutPromise(label, timeoutMs) {
|
|
23
|
+
return new Promise((_, reject) => {
|
|
24
|
+
setTimeout(() => reject(new Error(`${label} timed out after ${timeoutMs}ms`)), timeoutMs);
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
async function runModuleSetupHook(args) {
|
|
28
|
+
const startedAt = Date.now();
|
|
29
|
+
console.info("[onboarding.verify] module hook started", {
|
|
30
|
+
moduleId: args.moduleId,
|
|
31
|
+
phase: args.phase,
|
|
32
|
+
timeoutMs: args.timeoutMs
|
|
33
|
+
});
|
|
34
|
+
try {
|
|
35
|
+
await Promise.race([
|
|
36
|
+
args.run(),
|
|
37
|
+
createTimeoutPromise(`module ${args.moduleId} ${args.phase}`, args.timeoutMs)
|
|
38
|
+
]);
|
|
39
|
+
console.info("[onboarding.verify] module hook completed", {
|
|
40
|
+
moduleId: args.moduleId,
|
|
41
|
+
phase: args.phase,
|
|
42
|
+
durationMs: Math.max(0, Date.now() - startedAt)
|
|
43
|
+
});
|
|
44
|
+
} catch (error) {
|
|
45
|
+
if (isUniqueViolation(error)) {
|
|
46
|
+
console.info("[onboarding.verify] module hook skipped (already seeded)", {
|
|
47
|
+
moduleId: args.moduleId,
|
|
48
|
+
phase: args.phase,
|
|
49
|
+
durationMs: Math.max(0, Date.now() - startedAt)
|
|
50
|
+
});
|
|
51
|
+
} else {
|
|
52
|
+
console.error("[onboarding.verify] module hook failed", {
|
|
53
|
+
moduleId: args.moduleId,
|
|
54
|
+
phase: args.phase,
|
|
55
|
+
durationMs: Math.max(0, Date.now() - startedAt),
|
|
56
|
+
timeoutMs: args.timeoutMs,
|
|
57
|
+
error
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
async function markWorkspaceReady(args) {
|
|
64
|
+
const request = await args.service.findById(args.requestId);
|
|
65
|
+
if (!request || request.preparationCompletedAt || request.status !== "completed") return;
|
|
66
|
+
await args.service.markPreparationCompleted(request, /* @__PURE__ */ new Date());
|
|
67
|
+
}
|
|
68
|
+
async function enqueueVectorReindex(args) {
|
|
69
|
+
let searchIndexer = null;
|
|
70
|
+
try {
|
|
71
|
+
searchIndexer = args.container.resolve("searchIndexer");
|
|
72
|
+
} catch {
|
|
73
|
+
searchIndexer = null;
|
|
74
|
+
}
|
|
75
|
+
if (!searchIndexer) return;
|
|
76
|
+
await Promise.race([
|
|
77
|
+
searchIndexer.reindexAllToVector({
|
|
78
|
+
tenantId: args.tenantId,
|
|
79
|
+
organizationId: args.organizationId,
|
|
80
|
+
purgeFirst: true,
|
|
81
|
+
useQueue: true
|
|
82
|
+
}),
|
|
83
|
+
createTimeoutPromise("vector reindex enqueue", VECTOR_REINDEX_ENQUEUE_TIMEOUT_MS)
|
|
84
|
+
]);
|
|
85
|
+
}
|
|
86
|
+
async function rebuildTenantQueryIndexes(args) {
|
|
87
|
+
const coverageRefreshKeys = /* @__PURE__ */ new Set();
|
|
88
|
+
try {
|
|
89
|
+
const allEntities = getEntityIds();
|
|
90
|
+
const entityIds = flattenSystemEntityIds(allEntities);
|
|
91
|
+
for (const entityType of entityIds) {
|
|
92
|
+
try {
|
|
93
|
+
await purgeIndexScope(args.em, { entityType, tenantId: args.tenantId });
|
|
94
|
+
} catch (error) {
|
|
95
|
+
console.error("[onboarding.verify] failed to purge query index scope", {
|
|
96
|
+
entityType,
|
|
97
|
+
tenantId: args.tenantId,
|
|
98
|
+
error
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
try {
|
|
102
|
+
await reindexEntity(args.em, {
|
|
103
|
+
entityType,
|
|
104
|
+
tenantId: args.tenantId,
|
|
105
|
+
force: true,
|
|
106
|
+
emitVectorizeEvents: false,
|
|
107
|
+
vectorService: null
|
|
108
|
+
});
|
|
109
|
+
} catch (error) {
|
|
110
|
+
console.error("[onboarding.verify] failed to reindex entity", {
|
|
111
|
+
entityType,
|
|
112
|
+
tenantId: args.tenantId,
|
|
113
|
+
error
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
coverageRefreshKeys.add(`${entityType}|${args.tenantId}|__null__`);
|
|
117
|
+
coverageRefreshKeys.add(`${entityType}|${args.tenantId}|${args.organizationId}`);
|
|
118
|
+
}
|
|
119
|
+
} catch (error) {
|
|
120
|
+
console.error("[onboarding.verify] failed to rebuild query indexes", { tenantId: args.tenantId, error });
|
|
121
|
+
}
|
|
122
|
+
if (!coverageRefreshKeys.size) return;
|
|
123
|
+
for (const entry of coverageRefreshKeys) {
|
|
124
|
+
const [entityType, tenantKey, orgKey] = entry.split("|");
|
|
125
|
+
const orgScope = orgKey === "__null__" ? null : orgKey;
|
|
126
|
+
try {
|
|
127
|
+
await refreshCoverageSnapshot(
|
|
128
|
+
args.em,
|
|
129
|
+
{
|
|
130
|
+
entityType,
|
|
131
|
+
tenantId: tenantKey,
|
|
132
|
+
organizationId: orgScope,
|
|
133
|
+
withDeleted: false
|
|
134
|
+
}
|
|
135
|
+
);
|
|
136
|
+
} catch (error) {
|
|
137
|
+
console.error("[onboarding.verify] failed to refresh coverage snapshot", {
|
|
138
|
+
entityType,
|
|
139
|
+
tenantId: tenantKey,
|
|
140
|
+
organizationId: orgScope,
|
|
141
|
+
error
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
async function runDeferredProvisioning(args) {
|
|
147
|
+
const container = await createRequestContainer();
|
|
148
|
+
const em = container.resolve("em");
|
|
149
|
+
const service = new OnboardingService(em);
|
|
150
|
+
const claimedAt = /* @__PURE__ */ new Date();
|
|
151
|
+
const claimed = await service.claimPreparation(
|
|
152
|
+
args.requestId,
|
|
153
|
+
claimedAt,
|
|
154
|
+
new Date(claimedAt.getTime() - PREPARATION_CLAIM_STALE_MS)
|
|
155
|
+
);
|
|
156
|
+
if (!claimed) {
|
|
157
|
+
console.info("[onboarding.verify] deferred provisioning skipped (already claimed or completed)", {
|
|
158
|
+
requestId: args.requestId,
|
|
159
|
+
tenantId: args.tenantId
|
|
160
|
+
});
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
const modules = getModules();
|
|
164
|
+
for (const mod of modules) {
|
|
165
|
+
if (!mod.setup?.seedExamples) continue;
|
|
166
|
+
await service.renewPreparation(args.requestId, /* @__PURE__ */ new Date()).catch(() => {
|
|
167
|
+
});
|
|
168
|
+
try {
|
|
169
|
+
await runModuleSetupHook({
|
|
170
|
+
moduleId: mod.id,
|
|
171
|
+
phase: "seedExamples",
|
|
172
|
+
timeoutMs: SEED_EXAMPLES_TIMEOUT_MS,
|
|
173
|
+
run: () => mod.setup.seedExamples({
|
|
174
|
+
em,
|
|
175
|
+
tenantId: args.tenantId,
|
|
176
|
+
organizationId: args.organizationId,
|
|
177
|
+
container
|
|
178
|
+
})
|
|
179
|
+
});
|
|
180
|
+
} catch (error) {
|
|
181
|
+
if (isUniqueViolation(error)) {
|
|
182
|
+
console.info("[onboarding.verify] deferred seedExamples skipped (already applied)", {
|
|
183
|
+
moduleId: mod.id,
|
|
184
|
+
tenantId: args.tenantId,
|
|
185
|
+
organizationId: args.organizationId
|
|
186
|
+
});
|
|
187
|
+
} else {
|
|
188
|
+
console.error("[onboarding.verify] deferred seedExamples failed", {
|
|
189
|
+
moduleId: mod.id,
|
|
190
|
+
tenantId: args.tenantId,
|
|
191
|
+
organizationId: args.organizationId,
|
|
192
|
+
error
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
await service.renewPreparation(args.requestId, /* @__PURE__ */ new Date()).catch(() => {
|
|
198
|
+
});
|
|
199
|
+
await rebuildTenantQueryIndexes({
|
|
200
|
+
em,
|
|
201
|
+
tenantId: args.tenantId,
|
|
202
|
+
organizationId: args.organizationId
|
|
203
|
+
});
|
|
204
|
+
await markWorkspaceReady({
|
|
205
|
+
requestId: args.requestId,
|
|
206
|
+
service
|
|
207
|
+
});
|
|
208
|
+
await sendWorkspaceReadyEmail({
|
|
209
|
+
requestId: args.requestId,
|
|
210
|
+
tenantId: args.tenantId
|
|
211
|
+
}).catch((error) => {
|
|
212
|
+
console.error("[onboarding.verify] ready email failed", {
|
|
213
|
+
requestId: args.requestId,
|
|
214
|
+
tenantId: args.tenantId,
|
|
215
|
+
organizationId: args.organizationId,
|
|
216
|
+
error
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
await enqueueVectorReindex({
|
|
220
|
+
container,
|
|
221
|
+
tenantId: args.tenantId,
|
|
222
|
+
organizationId: args.organizationId
|
|
223
|
+
}).catch((error) => {
|
|
224
|
+
console.warn("[onboarding.verify] vector reindex enqueue did not complete promptly", {
|
|
225
|
+
tenantId: args.tenantId,
|
|
226
|
+
organizationId: args.organizationId,
|
|
227
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
export {
|
|
232
|
+
resolveProvisioningIds,
|
|
233
|
+
runDeferredProvisioning
|
|
234
|
+
};
|
|
235
|
+
//# sourceMappingURL=deferred-provisioning.js.map
|