@open-mercato/onboarding 0.4.2-canary-c02407ff85
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/build.mjs +62 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +7 -0
- package/dist/modules/onboarding/acl.js +11 -0
- package/dist/modules/onboarding/acl.js.map +7 -0
- package/dist/modules/onboarding/api/get/onboarding/verify.js +208 -0
- package/dist/modules/onboarding/api/get/onboarding/verify.js.map +7 -0
- package/dist/modules/onboarding/api/post/onboarding.js +193 -0
- package/dist/modules/onboarding/api/post/onboarding.js.map +7 -0
- package/dist/modules/onboarding/data/entities.js +84 -0
- package/dist/modules/onboarding/data/entities.js.map +7 -0
- package/dist/modules/onboarding/data/validators.js +27 -0
- package/dist/modules/onboarding/data/validators.js.map +7 -0
- package/dist/modules/onboarding/emails/AdminNotificationEmail.js +18 -0
- package/dist/modules/onboarding/emails/AdminNotificationEmail.js.map +7 -0
- package/dist/modules/onboarding/emails/VerificationEmail.js +36 -0
- package/dist/modules/onboarding/emails/VerificationEmail.js.map +7 -0
- package/dist/modules/onboarding/frontend/onboarding/page.js +279 -0
- package/dist/modules/onboarding/frontend/onboarding/page.js.map +7 -0
- package/dist/modules/onboarding/index.js +14 -0
- package/dist/modules/onboarding/index.js.map +7 -0
- package/dist/modules/onboarding/lib/service.js +83 -0
- package/dist/modules/onboarding/lib/service.js.map +7 -0
- package/dist/modules/onboarding/migrations/Migration20260112142945.js +12 -0
- package/dist/modules/onboarding/migrations/Migration20260112142945.js.map +7 -0
- package/generated/entities/onboarding_request/index.ts +19 -0
- package/generated/entities.ids.generated.ts +11 -0
- package/generated/entity-fields-registry.ts +11 -0
- package/jest.config.cjs +19 -0
- package/package.json +83 -0
- package/src/index.ts +2 -0
- package/src/modules/onboarding/acl.ts +7 -0
- package/src/modules/onboarding/api/get/onboarding/verify.ts +224 -0
- package/src/modules/onboarding/api/post/onboarding.ts +210 -0
- package/src/modules/onboarding/data/entities.ts +67 -0
- package/src/modules/onboarding/data/validators.ts +27 -0
- package/src/modules/onboarding/emails/AdminNotificationEmail.tsx +32 -0
- package/src/modules/onboarding/emails/VerificationEmail.tsx +54 -0
- package/src/modules/onboarding/frontend/onboarding/page.tsx +305 -0
- package/src/modules/onboarding/i18n/de.json +49 -0
- package/src/modules/onboarding/i18n/en.json +49 -0
- package/src/modules/onboarding/i18n/es.json +49 -0
- package/src/modules/onboarding/i18n/pl.json +49 -0
- package/src/modules/onboarding/index.ts +12 -0
- package/src/modules/onboarding/lib/service.ts +90 -0
- package/src/modules/onboarding/migrations/.snapshot-open-mercato.json +230 -0
- package/src/modules/onboarding/migrations/Migration20260112142945.ts +11 -0
- package/tsconfig.build.json +4 -0
- package/tsconfig.json +9 -0
- package/watch.mjs +6 -0
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/onboarding/data/entities.ts"],
|
|
4
|
+
"sourcesContent": ["import { Entity, PrimaryKey, Property, Unique } from '@mikro-orm/core'\n\ntype OnboardingStatus = 'pending' | '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: 'password_hash', type: 'text', nullable: true })\n passwordHash?: string | 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: '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;AAwBzB,qBAAkB,oBAAI,KAAK;AAAA;AAO7B;AAvDE;AAAA,EADC,WAAW,EAAE,MAAM,QAAQ,YAAY,oBAAoB,CAAC;AAAA,GADlD,kBAEX;AAGA;AAAA,EADC,SAAS,EAAE,MAAM,OAAO,CAAC;AAAA,GAJf,kBAKX;AAGA;AAAA,EADC,SAAS,EAAE,MAAM,cAAc,MAAM,OAAO,CAAC;AAAA,GAPnC,kBAQX;AAGA;AAAA,EADC,SAAS,EAAE,MAAM,QAAQ,SAAS,UAAU,CAAC;AAAA,GAVnC,kBAWX;AAGA;AAAA,EADC,SAAS,EAAE,MAAM,cAAc,MAAM,OAAO,CAAC;AAAA,GAbnC,kBAcX;AAGA;AAAA,EADC,SAAS,EAAE,MAAM,aAAa,MAAM,OAAO,CAAC;AAAA,GAhBlC,kBAiBX;AAGA;AAAA,EADC,SAAS,EAAE,MAAM,qBAAqB,MAAM,OAAO,CAAC;AAAA,GAnB1C,kBAoBX;AAGA;AAAA,EADC,SAAS,EAAE,MAAM,QAAQ,UAAU,KAAK,CAAC;AAAA,GAtB/B,kBAuBX;AAGA;AAAA,EADC,SAAS,EAAE,MAAM,kBAAkB,MAAM,WAAW,SAAS,MAAM,CAAC;AAAA,GAzB1D,kBA0BX;AAGA;AAAA,EADC,SAAS,EAAE,MAAM,iBAAiB,MAAM,QAAQ,UAAU,KAAK,CAAC;AAAA,GA5BtD,kBA6BX;AAGA;AAAA,EADC,SAAS,EAAE,MAAM,cAAc,MAAM,KAAK,CAAC;AAAA,GA/BjC,kBAgCX;AAGA;AAAA,EADC,SAAS,EAAE,MAAM,gBAAgB,MAAM,MAAM,UAAU,KAAK,CAAC;AAAA,GAlCnD,kBAmCX;AAGA;AAAA,EADC,SAAS,EAAE,MAAM,aAAa,MAAM,QAAQ,UAAU,KAAK,CAAC;AAAA,GArClD,kBAsCX;AAGA;AAAA,EADC,SAAS,EAAE,MAAM,mBAAmB,MAAM,QAAQ,UAAU,KAAK,CAAC;AAAA,GAxCxD,kBAyCX;AAGA;AAAA,EADC,SAAS,EAAE,MAAM,WAAW,MAAM,QAAQ,UAAU,KAAK,CAAC;AAAA,GA3ChD,kBA4CX;AAGA;AAAA,EADC,SAAS,EAAE,MAAM,sBAAsB,MAAM,MAAM,UAAU,KAAK,CAAC;AAAA,GA9CzD,kBA+CX;AAGA;AAAA,EADC,SAAS,EAAE,MAAM,cAAc,MAAM,MAAM,UAAU,MAAM,oBAAI,KAAK,EAAE,CAAC;AAAA,GAjD7D,kBAkDX;AAGA;AAAA,EADC,SAAS,EAAE,MAAM,cAAc,MAAM,MAAM,UAAU,MAAM,oBAAI,KAAK,GAAG,UAAU,KAAK,CAAC;AAAA,GApD7E,kBAqDX;AAGA;AAAA,EADC,SAAS,EAAE,MAAM,cAAc,MAAM,MAAM,UAAU,KAAK,CAAC;AAAA,GAvDjD,kBAwDX;AAxDW,oBAAN;AAAA,EAHN,OAAO,EAAE,WAAW,sBAAsB,CAAC;AAAA,EAC3C,OAAO,EAAE,YAAY,CAAC,OAAO,EAAE,CAAC;AAAA,EAChC,OAAO,EAAE,YAAY,CAAC,WAAW,EAAE,CAAC;AAAA,GACxB;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
const onboardingStartSchema = z.object({
|
|
3
|
+
email: z.string().email(),
|
|
4
|
+
firstName: z.string().min(1).max(120),
|
|
5
|
+
lastName: z.string().min(1).max(120),
|
|
6
|
+
organizationName: z.string().min(1).max(240),
|
|
7
|
+
password: z.string().min(6).max(120),
|
|
8
|
+
confirmPassword: z.string().min(6).max(120),
|
|
9
|
+
termsAccepted: z.literal(true),
|
|
10
|
+
locale: z.string().min(2).max(10).optional()
|
|
11
|
+
}).superRefine((value, ctx) => {
|
|
12
|
+
if (value.password !== value.confirmPassword) {
|
|
13
|
+
ctx.addIssue({
|
|
14
|
+
code: z.ZodIssueCode.custom,
|
|
15
|
+
message: "Passwords must match.",
|
|
16
|
+
path: ["confirmPassword"]
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
const onboardingVerifySchema = z.object({
|
|
21
|
+
token: z.string().min(32)
|
|
22
|
+
});
|
|
23
|
+
export {
|
|
24
|
+
onboardingStartSchema,
|
|
25
|
+
onboardingVerifySchema
|
|
26
|
+
};
|
|
27
|
+
//# sourceMappingURL=validators.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/onboarding/data/validators.ts"],
|
|
4
|
+
"sourcesContent": ["import { z } from 'zod'\n\nexport const onboardingStartSchema = z.object({\n email: z.string().email(),\n firstName: z.string().min(1).max(120),\n lastName: z.string().min(1).max(120),\n organizationName: z.string().min(1).max(240),\n password: z.string().min(6).max(120),\n confirmPassword: z.string().min(6).max(120),\n termsAccepted: z.literal(true),\n locale: z.string().min(2).max(10).optional(),\n}).superRefine((value, ctx) => {\n if (value.password !== value.confirmPassword) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: 'Passwords must match.',\n path: ['confirmPassword'],\n })\n }\n})\n\nexport const onboardingVerifySchema = z.object({\n token: z.string().min(32),\n})\n\nexport type OnboardingStartInput = z.infer<typeof onboardingStartSchema>\nexport type OnboardingVerifyInput = z.infer<typeof onboardingVerifySchema>\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,SAAS;AAEX,MAAM,wBAAwB,EAAE,OAAO;AAAA,EAC5C,OAAO,EAAE,OAAO,EAAE,MAAM;AAAA,EACxB,WAAW,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG;AAAA,EACpC,UAAU,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG;AAAA,EACnC,kBAAkB,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG;AAAA,EAC3C,UAAU,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG;AAAA,EACnC,iBAAiB,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG;AAAA,EAC1C,eAAe,EAAE,QAAQ,IAAI;AAAA,EAC7B,QAAQ,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,EAAE,EAAE,SAAS;AAC7C,CAAC,EAAE,YAAY,CAAC,OAAO,QAAQ;AAC7B,MAAI,MAAM,aAAa,MAAM,iBAAiB;AAC5C,QAAI,SAAS;AAAA,MACX,MAAM,EAAE,aAAa;AAAA,MACrB,SAAS;AAAA,MACT,MAAM,CAAC,iBAAiB;AAAA,IAC1B,CAAC;AAAA,EACH;AACF,CAAC;AAEM,MAAM,yBAAyB,EAAE,OAAO;AAAA,EAC7C,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE;AAC1B,CAAC;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Html, Head, Preview, Body, Container, Heading, Text, Hr } from "@react-email/components";
|
|
3
|
+
function AdminNotificationEmail({ copy }) {
|
|
4
|
+
return /* @__PURE__ */ jsxs(Html, { children: [
|
|
5
|
+
/* @__PURE__ */ jsx(Head, { children: /* @__PURE__ */ jsx("title", { children: copy.heading }) }),
|
|
6
|
+
/* @__PURE__ */ jsx(Preview, { children: copy.preview }),
|
|
7
|
+
/* @__PURE__ */ jsx(Body, { style: { backgroundColor: "#f8fafc", fontFamily: "Helvetica, Arial, sans-serif", padding: "24px 0" }, children: /* @__PURE__ */ jsxs(Container, { style: { backgroundColor: "#ffffff", padding: "28px", borderRadius: "12px", margin: "0 auto", maxWidth: "520px" }, children: [
|
|
8
|
+
/* @__PURE__ */ jsx(Heading, { style: { fontSize: "22px", fontWeight: 600, margin: "0 0 16px", color: "#0f172a" }, children: copy.heading }),
|
|
9
|
+
/* @__PURE__ */ jsx(Text, { style: { fontSize: "15px", color: "#1f2937", lineHeight: "24px", marginBottom: "20px" }, children: copy.body }),
|
|
10
|
+
/* @__PURE__ */ jsx(Hr, { style: { borderColor: "#e2e8f0", margin: "24px 0" } }),
|
|
11
|
+
/* @__PURE__ */ jsx(Text, { style: { fontSize: "13px", color: "#64748b" }, children: copy.footer })
|
|
12
|
+
] }) })
|
|
13
|
+
] });
|
|
14
|
+
}
|
|
15
|
+
export {
|
|
16
|
+
AdminNotificationEmail as default
|
|
17
|
+
};
|
|
18
|
+
//# sourceMappingURL=AdminNotificationEmail.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/onboarding/emails/AdminNotificationEmail.tsx"],
|
|
4
|
+
"sourcesContent": ["import React from 'react'\nimport { Html, Head, Preview, Body, Container, Heading, Text, Hr } from '@react-email/components'\n\nexport type AdminNotificationCopy = {\n preview: string\n heading: string\n body: string\n footer: string\n}\n\ntype AdminNotificationEmailProps = {\n copy: AdminNotificationCopy\n}\n\nexport default function AdminNotificationEmail({ copy }: AdminNotificationEmailProps) {\n return (\n <Html>\n <Head>\n <title>{copy.heading}</title>\n </Head>\n <Preview>{copy.preview}</Preview>\n <Body style={{ backgroundColor: '#f8fafc', fontFamily: 'Helvetica, Arial, sans-serif', padding: '24px 0' }}>\n <Container style={{ backgroundColor: '#ffffff', padding: '28px', borderRadius: '12px', margin: '0 auto', maxWidth: '520px' }}>\n <Heading style={{ fontSize: '22px', fontWeight: 600, margin: '0 0 16px', color: '#0f172a' }}>{copy.heading}</Heading>\n <Text style={{ fontSize: '15px', color: '#1f2937', lineHeight: '24px', marginBottom: '20px' }}>{copy.body}</Text>\n <Hr style={{ borderColor: '#e2e8f0', margin: '24px 0' }} />\n <Text style={{ fontSize: '13px', color: '#64748b' }}>{copy.footer}</Text>\n </Container>\n </Body>\n </Html>\n )\n}\n"],
|
|
5
|
+
"mappings": "AAkBQ,cAIA,YAJA;AAjBR,SAAS,MAAM,MAAM,SAAS,MAAM,WAAW,SAAS,MAAM,UAAU;AAazD,SAAR,uBAAwC,EAAE,KAAK,GAAgC;AACpF,SACE,qBAAC,QACC;AAAA,wBAAC,QACC,8BAAC,WAAO,eAAK,SAAQ,GACvB;AAAA,IACA,oBAAC,WAAS,eAAK,SAAQ;AAAA,IACvB,oBAAC,QAAK,OAAO,EAAE,iBAAiB,WAAW,YAAY,gCAAgC,SAAS,SAAS,GACvG,+BAAC,aAAU,OAAO,EAAE,iBAAiB,WAAW,SAAS,QAAQ,cAAc,QAAQ,QAAQ,UAAU,UAAU,QAAQ,GACzH;AAAA,0BAAC,WAAQ,OAAO,EAAE,UAAU,QAAQ,YAAY,KAAK,QAAQ,YAAY,OAAO,UAAU,GAAI,eAAK,SAAQ;AAAA,MAC3G,oBAAC,QAAK,OAAO,EAAE,UAAU,QAAQ,OAAO,WAAW,YAAY,QAAQ,cAAc,OAAO,GAAI,eAAK,MAAK;AAAA,MAC1G,oBAAC,MAAG,OAAO,EAAE,aAAa,WAAW,QAAQ,SAAS,GAAG;AAAA,MACzD,oBAAC,QAAK,OAAO,EAAE,UAAU,QAAQ,OAAO,UAAU,GAAI,eAAK,QAAO;AAAA,OACpE,GACF;AAAA,KACF;AAEJ;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Html, Head, Preview, Body, Container, Heading, Text, Section, Button, Hr } from "@react-email/components";
|
|
3
|
+
function VerificationEmail({ verifyUrl, copy }) {
|
|
4
|
+
return /* @__PURE__ */ jsxs(Html, { children: [
|
|
5
|
+
/* @__PURE__ */ jsx(Head, { children: /* @__PURE__ */ jsx("title", { children: copy.heading }) }),
|
|
6
|
+
/* @__PURE__ */ jsx(Preview, { children: copy.preview }),
|
|
7
|
+
/* @__PURE__ */ jsx(Body, { style: { backgroundColor: "#f1f5f9", fontFamily: "Helvetica, Arial, sans-serif", padding: "24px 0" }, children: /* @__PURE__ */ jsxs(Container, { style: { backgroundColor: "#ffffff", padding: "32px", borderRadius: "12px", margin: "0 auto", maxWidth: "520px" }, children: [
|
|
8
|
+
/* @__PURE__ */ jsx(Heading, { style: { fontSize: "24px", fontWeight: 600, margin: "0 0 16px" }, children: copy.heading }),
|
|
9
|
+
/* @__PURE__ */ jsx(Text, { style: { fontSize: "16px", color: "#334155", marginBottom: "16px" }, children: copy.greeting }),
|
|
10
|
+
/* @__PURE__ */ jsx(Text, { style: { fontSize: "16px", color: "#334155", marginBottom: "16px", lineHeight: "24px" }, children: copy.body }),
|
|
11
|
+
/* @__PURE__ */ jsx(Section, { style: { textAlign: "center", margin: "32px 0" }, children: /* @__PURE__ */ jsx(
|
|
12
|
+
Button,
|
|
13
|
+
{
|
|
14
|
+
href: verifyUrl,
|
|
15
|
+
style: {
|
|
16
|
+
backgroundColor: "#111827",
|
|
17
|
+
color: "#ffffff",
|
|
18
|
+
padding: "12px 24px",
|
|
19
|
+
borderRadius: "8px",
|
|
20
|
+
fontSize: "15px",
|
|
21
|
+
textDecoration: "none",
|
|
22
|
+
display: "inline-block"
|
|
23
|
+
},
|
|
24
|
+
children: copy.cta
|
|
25
|
+
}
|
|
26
|
+
) }),
|
|
27
|
+
/* @__PURE__ */ jsx(Text, { style: { fontSize: "14px", color: "#64748b", marginBottom: "16px", lineHeight: "22px" }, children: copy.expiry }),
|
|
28
|
+
/* @__PURE__ */ jsx(Hr, { style: { borderColor: "#e2e8f0", margin: "24px 0" } }),
|
|
29
|
+
/* @__PURE__ */ jsx(Text, { style: { fontSize: "12px", color: "#94a3b8" }, children: copy.footer })
|
|
30
|
+
] }) })
|
|
31
|
+
] });
|
|
32
|
+
}
|
|
33
|
+
export {
|
|
34
|
+
VerificationEmail as default
|
|
35
|
+
};
|
|
36
|
+
//# sourceMappingURL=VerificationEmail.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/onboarding/emails/VerificationEmail.tsx"],
|
|
4
|
+
"sourcesContent": ["import React from 'react'\nimport { Html, Head, Preview, Body, Container, Heading, Text, Section, Button, Hr } from '@react-email/components'\n\nexport type VerificationEmailCopy = {\n preview: string\n heading: string\n greeting: string\n body: string\n cta: string\n expiry: string\n footer: string\n}\n\ntype VerificationEmailProps = {\n verifyUrl: string\n copy: VerificationEmailCopy\n}\n\nexport default function VerificationEmail({ verifyUrl, copy }: VerificationEmailProps) {\n return (\n <Html>\n <Head>\n <title>{copy.heading}</title>\n </Head>\n <Preview>{copy.preview}</Preview>\n <Body style={{ backgroundColor: '#f1f5f9', fontFamily: 'Helvetica, Arial, sans-serif', padding: '24px 0' }}>\n <Container style={{ backgroundColor: '#ffffff', padding: '32px', borderRadius: '12px', margin: '0 auto', maxWidth: '520px' }}>\n <Heading style={{ fontSize: '24px', fontWeight: 600, margin: '0 0 16px' }}>{copy.heading}</Heading>\n <Text style={{ fontSize: '16px', color: '#334155', marginBottom: '16px' }}>{copy.greeting}</Text>\n <Text style={{ fontSize: '16px', color: '#334155', marginBottom: '16px', lineHeight: '24px' }}>{copy.body}</Text>\n <Section style={{ textAlign: 'center', margin: '32px 0' }}>\n <Button\n href={verifyUrl}\n style={{\n backgroundColor: '#111827',\n color: '#ffffff',\n padding: '12px 24px',\n borderRadius: '8px',\n fontSize: '15px',\n textDecoration: 'none',\n display: 'inline-block',\n }}\n >\n {copy.cta}\n </Button>\n </Section>\n <Text style={{ fontSize: '14px', color: '#64748b', marginBottom: '16px', lineHeight: '22px' }}>{copy.expiry}</Text>\n <Hr style={{ borderColor: '#e2e8f0', margin: '24px 0' }} />\n <Text style={{ fontSize: '12px', color: '#94a3b8' }}>{copy.footer}</Text>\n </Container>\n </Body>\n </Html>\n )\n}\n"],
|
|
5
|
+
"mappings": "AAsBQ,cAIA,YAJA;AArBR,SAAS,MAAM,MAAM,SAAS,MAAM,WAAW,SAAS,MAAM,SAAS,QAAQ,UAAU;AAiB1E,SAAR,kBAAmC,EAAE,WAAW,KAAK,GAA2B;AACrF,SACE,qBAAC,QACC;AAAA,wBAAC,QACC,8BAAC,WAAO,eAAK,SAAQ,GACvB;AAAA,IACA,oBAAC,WAAS,eAAK,SAAQ;AAAA,IACvB,oBAAC,QAAK,OAAO,EAAE,iBAAiB,WAAW,YAAY,gCAAgC,SAAS,SAAS,GACvG,+BAAC,aAAU,OAAO,EAAE,iBAAiB,WAAW,SAAS,QAAQ,cAAc,QAAQ,QAAQ,UAAU,UAAU,QAAQ,GACzH;AAAA,0BAAC,WAAQ,OAAO,EAAE,UAAU,QAAQ,YAAY,KAAK,QAAQ,WAAW,GAAI,eAAK,SAAQ;AAAA,MACzF,oBAAC,QAAK,OAAO,EAAE,UAAU,QAAQ,OAAO,WAAW,cAAc,OAAO,GAAI,eAAK,UAAS;AAAA,MAC1F,oBAAC,QAAK,OAAO,EAAE,UAAU,QAAQ,OAAO,WAAW,cAAc,QAAQ,YAAY,OAAO,GAAI,eAAK,MAAK;AAAA,MAC1G,oBAAC,WAAQ,OAAO,EAAE,WAAW,UAAU,QAAQ,SAAS,GACtD;AAAA,QAAC;AAAA;AAAA,UACC,MAAM;AAAA,UACN,OAAO;AAAA,YACL,iBAAiB;AAAA,YACjB,OAAO;AAAA,YACP,SAAS;AAAA,YACT,cAAc;AAAA,YACd,UAAU;AAAA,YACV,gBAAgB;AAAA,YAChB,SAAS;AAAA,UACX;AAAA,UAEC,eAAK;AAAA;AAAA,MACR,GACF;AAAA,MACA,oBAAC,QAAK,OAAO,EAAE,UAAU,QAAQ,OAAO,WAAW,cAAc,QAAQ,YAAY,OAAO,GAAI,eAAK,QAAO;AAAA,MAC5G,oBAAC,MAAG,OAAO,EAAE,aAAa,WAAW,QAAQ,SAAS,GAAG;AAAA,MACzD,oBAAC,QAAK,OAAO,EAAE,UAAU,QAAQ,OAAO,UAAU,GAAI,eAAK,QAAO;AAAA,OACpE,GACF;AAAA,KACF;AAEJ;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import Image from "next/image";
|
|
4
|
+
import { useState } from "react";
|
|
5
|
+
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@open-mercato/ui/primitives/card";
|
|
6
|
+
import { Input } from "@open-mercato/ui/primitives/input";
|
|
7
|
+
import { Label } from "@open-mercato/ui/primitives/label";
|
|
8
|
+
import { Checkbox } from "@open-mercato/ui/primitives/checkbox";
|
|
9
|
+
import { useT, useLocale } from "@open-mercato/shared/lib/i18n/context";
|
|
10
|
+
import { translateWithFallback } from "@open-mercato/shared/lib/i18n/translate";
|
|
11
|
+
import { apiCall } from "@open-mercato/ui/backend/utils/apiCall";
|
|
12
|
+
import { onboardingStartSchema } from "@open-mercato/onboarding/modules/onboarding/data/validators";
|
|
13
|
+
function OnboardingPage() {
|
|
14
|
+
const t = useT();
|
|
15
|
+
const translate = (key, fallback, params) => translateWithFallback(t, key, fallback, params);
|
|
16
|
+
const locale = useLocale();
|
|
17
|
+
const [state, setState] = useState("idle");
|
|
18
|
+
const [globalError, setGlobalError] = useState(null);
|
|
19
|
+
const [fieldErrors, setFieldErrors] = useState({});
|
|
20
|
+
const [termsAccepted, setTermsAccepted] = useState(false);
|
|
21
|
+
const [emailSubmitted, setEmailSubmitted] = useState(null);
|
|
22
|
+
async function onSubmit(event) {
|
|
23
|
+
event.preventDefault();
|
|
24
|
+
setState("loading");
|
|
25
|
+
setGlobalError(null);
|
|
26
|
+
setFieldErrors({});
|
|
27
|
+
const form = new FormData(event.currentTarget);
|
|
28
|
+
const payload = {
|
|
29
|
+
email: String(form.get("email") ?? "").trim(),
|
|
30
|
+
firstName: String(form.get("firstName") ?? "").trim(),
|
|
31
|
+
lastName: String(form.get("lastName") ?? "").trim(),
|
|
32
|
+
organizationName: String(form.get("organizationName") ?? "").trim(),
|
|
33
|
+
password: String(form.get("password") ?? ""),
|
|
34
|
+
confirmPassword: String(form.get("confirmPassword") ?? ""),
|
|
35
|
+
termsAccepted,
|
|
36
|
+
locale
|
|
37
|
+
};
|
|
38
|
+
const parsed = onboardingStartSchema.safeParse(payload);
|
|
39
|
+
if (!parsed.success) {
|
|
40
|
+
const issueMap = {};
|
|
41
|
+
parsed.error.issues.forEach((issue) => {
|
|
42
|
+
const path = issue.path[0];
|
|
43
|
+
if (!path) return;
|
|
44
|
+
switch (path) {
|
|
45
|
+
case "email":
|
|
46
|
+
issueMap.email = translate("onboarding.errors.emailInvalid", "Enter a valid work email.");
|
|
47
|
+
break;
|
|
48
|
+
case "firstName":
|
|
49
|
+
issueMap.firstName = translate("onboarding.errors.firstNameRequired", "First name is required.");
|
|
50
|
+
break;
|
|
51
|
+
case "lastName":
|
|
52
|
+
issueMap.lastName = translate("onboarding.errors.lastNameRequired", "Last name is required.");
|
|
53
|
+
break;
|
|
54
|
+
case "organizationName":
|
|
55
|
+
issueMap.organizationName = translate("onboarding.errors.organizationNameRequired", "Organization name is required.");
|
|
56
|
+
break;
|
|
57
|
+
case "password":
|
|
58
|
+
issueMap.password = translate("onboarding.errors.passwordRequired", "Password must be at least 6 characters.");
|
|
59
|
+
break;
|
|
60
|
+
case "confirmPassword":
|
|
61
|
+
issueMap.confirmPassword = translate("onboarding.errors.passwordMismatch", "Passwords must match.");
|
|
62
|
+
break;
|
|
63
|
+
case "termsAccepted":
|
|
64
|
+
issueMap.termsAccepted = translate("onboarding.form.termsRequired", "Please accept the terms to continue.");
|
|
65
|
+
break;
|
|
66
|
+
default:
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
if (!issueMap.termsAccepted && !termsAccepted) {
|
|
71
|
+
issueMap.termsAccepted = translate("onboarding.form.termsRequired", "Please accept the terms to continue.");
|
|
72
|
+
}
|
|
73
|
+
setFieldErrors(issueMap);
|
|
74
|
+
setState("idle");
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
const call = await apiCall(
|
|
79
|
+
"/api/onboarding/onboarding",
|
|
80
|
+
{
|
|
81
|
+
method: "POST",
|
|
82
|
+
headers: { "content-type": "application/json" },
|
|
83
|
+
body: JSON.stringify({ ...parsed.data, termsAccepted: true })
|
|
84
|
+
}
|
|
85
|
+
);
|
|
86
|
+
const data = call.result ?? {};
|
|
87
|
+
if (!call.ok || data.ok === false) {
|
|
88
|
+
if (data.fieldErrors && typeof data.fieldErrors === "object") {
|
|
89
|
+
const mapped = {};
|
|
90
|
+
for (const key of Object.keys(data.fieldErrors)) {
|
|
91
|
+
const value = data.fieldErrors[key];
|
|
92
|
+
if (typeof value === "string" && value.trim()) {
|
|
93
|
+
mapped[key] = value;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
setFieldErrors(mapped);
|
|
97
|
+
}
|
|
98
|
+
const message = typeof data.error === "string" && data.error.trim() ? data.error : translate("onboarding.form.genericError", "Something went wrong. Please try again.");
|
|
99
|
+
setGlobalError(message);
|
|
100
|
+
setState("idle");
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
setEmailSubmitted(data.email ?? parsed.data.email);
|
|
104
|
+
setState("success");
|
|
105
|
+
} catch (err) {
|
|
106
|
+
const message = err instanceof Error ? err.message : "";
|
|
107
|
+
setGlobalError(message || translate("onboarding.form.genericError", "Something went wrong. Please try again."));
|
|
108
|
+
setState("idle");
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const submitting = state === "loading";
|
|
112
|
+
const disabled = submitting || state === "success";
|
|
113
|
+
return /* @__PURE__ */ jsx("div", { className: "relative min-h-svh flex items-center justify-center bg-muted/40 px-4 pb-24", children: /* @__PURE__ */ jsxs(Card, { className: "w-full max-w-lg shadow-lg", children: [
|
|
114
|
+
/* @__PURE__ */ jsx(CardHeader, { className: "flex flex-col gap-4 p-10 text-center", children: /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center gap-3", children: [
|
|
115
|
+
/* @__PURE__ */ jsx(Image, { alt: "Open Mercato", src: "/open-mercato.svg", width: 120, height: 120, priority: true }),
|
|
116
|
+
/* @__PURE__ */ jsx(CardTitle, { className: "text-2xl font-semibold", children: translate("onboarding.title", "Create your Open Mercato workspace") }),
|
|
117
|
+
/* @__PURE__ */ jsx(CardDescription, { children: translate("onboarding.subtitle", "Tell us a bit about you and we will set everything up.") })
|
|
118
|
+
] }) }),
|
|
119
|
+
/* @__PURE__ */ jsxs(CardContent, { className: "pb-10", children: [
|
|
120
|
+
state === "success" && emailSubmitted && /* @__PURE__ */ jsxs("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", children: [
|
|
121
|
+
/* @__PURE__ */ jsx("strong", { className: "block text-sm font-medium", children: translate("onboarding.form.successTitle", "Check your inbox") }),
|
|
122
|
+
/* @__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 }) })
|
|
123
|
+
] }),
|
|
124
|
+
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 }),
|
|
125
|
+
/* @__PURE__ */ jsxs("form", { className: "grid gap-4", onSubmit, noValidate: true, children: [
|
|
126
|
+
/* @__PURE__ */ jsxs("div", { className: "grid gap-1", children: [
|
|
127
|
+
/* @__PURE__ */ jsx(Label, { htmlFor: "email", children: translate("onboarding.form.email", "Work email") }),
|
|
128
|
+
/* @__PURE__ */ jsx(
|
|
129
|
+
Input,
|
|
130
|
+
{
|
|
131
|
+
id: "email",
|
|
132
|
+
name: "email",
|
|
133
|
+
type: "email",
|
|
134
|
+
required: true,
|
|
135
|
+
disabled,
|
|
136
|
+
autoComplete: "email",
|
|
137
|
+
"aria-invalid": Boolean(fieldErrors.email),
|
|
138
|
+
"aria-describedby": fieldErrors.email ? "email-error" : void 0,
|
|
139
|
+
className: fieldErrors.email ? "border-red-500 focus-visible:ring-red-500" : void 0
|
|
140
|
+
}
|
|
141
|
+
),
|
|
142
|
+
fieldErrors.email && /* @__PURE__ */ jsx("p", { id: "email-error", className: "text-xs text-red-600", children: fieldErrors.email })
|
|
143
|
+
] }),
|
|
144
|
+
/* @__PURE__ */ jsxs("div", { className: "grid gap-1 sm:grid-cols-2 sm:gap-4", children: [
|
|
145
|
+
/* @__PURE__ */ jsxs("div", { className: "grid gap-1", children: [
|
|
146
|
+
/* @__PURE__ */ jsx(Label, { htmlFor: "firstName", children: translate("onboarding.form.firstName", "First name") }),
|
|
147
|
+
/* @__PURE__ */ jsx(
|
|
148
|
+
Input,
|
|
149
|
+
{
|
|
150
|
+
id: "firstName",
|
|
151
|
+
name: "firstName",
|
|
152
|
+
type: "text",
|
|
153
|
+
required: true,
|
|
154
|
+
disabled,
|
|
155
|
+
autoComplete: "given-name",
|
|
156
|
+
"aria-invalid": Boolean(fieldErrors.firstName),
|
|
157
|
+
"aria-describedby": fieldErrors.firstName ? "firstName-error" : void 0,
|
|
158
|
+
className: fieldErrors.firstName ? "border-red-500 focus-visible:ring-red-500" : void 0
|
|
159
|
+
}
|
|
160
|
+
),
|
|
161
|
+
fieldErrors.firstName && /* @__PURE__ */ jsx("p", { id: "firstName-error", className: "text-xs text-red-600", children: fieldErrors.firstName })
|
|
162
|
+
] }),
|
|
163
|
+
/* @__PURE__ */ jsxs("div", { className: "grid gap-1", children: [
|
|
164
|
+
/* @__PURE__ */ jsx(Label, { htmlFor: "lastName", children: translate("onboarding.form.lastName", "Last name") }),
|
|
165
|
+
/* @__PURE__ */ jsx(
|
|
166
|
+
Input,
|
|
167
|
+
{
|
|
168
|
+
id: "lastName",
|
|
169
|
+
name: "lastName",
|
|
170
|
+
type: "text",
|
|
171
|
+
required: true,
|
|
172
|
+
disabled,
|
|
173
|
+
autoComplete: "family-name",
|
|
174
|
+
"aria-invalid": Boolean(fieldErrors.lastName),
|
|
175
|
+
"aria-describedby": fieldErrors.lastName ? "lastName-error" : void 0,
|
|
176
|
+
className: fieldErrors.lastName ? "border-red-500 focus-visible:ring-red-500" : void 0
|
|
177
|
+
}
|
|
178
|
+
),
|
|
179
|
+
fieldErrors.lastName && /* @__PURE__ */ jsx("p", { id: "lastName-error", className: "text-xs text-red-600", children: fieldErrors.lastName })
|
|
180
|
+
] })
|
|
181
|
+
] }),
|
|
182
|
+
/* @__PURE__ */ jsxs("div", { className: "grid gap-1", children: [
|
|
183
|
+
/* @__PURE__ */ jsx(Label, { htmlFor: "organizationName", children: translate("onboarding.form.organizationName", "Organization name") }),
|
|
184
|
+
/* @__PURE__ */ jsx(
|
|
185
|
+
Input,
|
|
186
|
+
{
|
|
187
|
+
id: "organizationName",
|
|
188
|
+
name: "organizationName",
|
|
189
|
+
type: "text",
|
|
190
|
+
required: true,
|
|
191
|
+
disabled,
|
|
192
|
+
autoComplete: "organization",
|
|
193
|
+
"aria-invalid": Boolean(fieldErrors.organizationName),
|
|
194
|
+
"aria-describedby": fieldErrors.organizationName ? "organizationName-error" : void 0,
|
|
195
|
+
className: fieldErrors.organizationName ? "border-red-500 focus-visible:ring-red-500" : void 0
|
|
196
|
+
}
|
|
197
|
+
),
|
|
198
|
+
fieldErrors.organizationName && /* @__PURE__ */ jsx("p", { id: "organizationName-error", className: "text-xs text-red-600", children: fieldErrors.organizationName })
|
|
199
|
+
] }),
|
|
200
|
+
/* @__PURE__ */ jsxs("div", { className: "grid gap-1", children: [
|
|
201
|
+
/* @__PURE__ */ jsx(Label, { htmlFor: "password", children: translate("onboarding.form.password", "Password") }),
|
|
202
|
+
/* @__PURE__ */ jsx(
|
|
203
|
+
Input,
|
|
204
|
+
{
|
|
205
|
+
id: "password",
|
|
206
|
+
name: "password",
|
|
207
|
+
type: "password",
|
|
208
|
+
required: true,
|
|
209
|
+
disabled,
|
|
210
|
+
autoComplete: "new-password",
|
|
211
|
+
"aria-invalid": Boolean(fieldErrors.password),
|
|
212
|
+
"aria-describedby": fieldErrors.password ? "password-error" : void 0,
|
|
213
|
+
className: fieldErrors.password ? "border-red-500 focus-visible:ring-red-500" : void 0
|
|
214
|
+
}
|
|
215
|
+
),
|
|
216
|
+
fieldErrors.password && /* @__PURE__ */ jsx("p", { id: "password-error", className: "text-xs text-red-600", children: fieldErrors.password })
|
|
217
|
+
] }),
|
|
218
|
+
/* @__PURE__ */ jsxs("div", { className: "grid gap-1", children: [
|
|
219
|
+
/* @__PURE__ */ jsx(Label, { htmlFor: "confirmPassword", children: translate("onboarding.form.confirmPassword", "Confirm password") }),
|
|
220
|
+
/* @__PURE__ */ jsx(
|
|
221
|
+
Input,
|
|
222
|
+
{
|
|
223
|
+
id: "confirmPassword",
|
|
224
|
+
name: "confirmPassword",
|
|
225
|
+
type: "password",
|
|
226
|
+
required: true,
|
|
227
|
+
disabled,
|
|
228
|
+
autoComplete: "new-password",
|
|
229
|
+
"aria-invalid": Boolean(fieldErrors.confirmPassword),
|
|
230
|
+
"aria-describedby": fieldErrors.confirmPassword ? "confirmPassword-error" : void 0,
|
|
231
|
+
className: fieldErrors.confirmPassword ? "border-red-500 focus-visible:ring-red-500" : void 0
|
|
232
|
+
}
|
|
233
|
+
),
|
|
234
|
+
fieldErrors.confirmPassword && /* @__PURE__ */ jsx("p", { id: "confirmPassword-error", className: "text-xs text-red-600", children: fieldErrors.confirmPassword })
|
|
235
|
+
] }),
|
|
236
|
+
/* @__PURE__ */ jsxs("label", { className: "flex items-start gap-3 text-sm text-muted-foreground", children: [
|
|
237
|
+
/* @__PURE__ */ jsx(
|
|
238
|
+
Checkbox,
|
|
239
|
+
{
|
|
240
|
+
id: "terms",
|
|
241
|
+
checked: termsAccepted,
|
|
242
|
+
disabled,
|
|
243
|
+
onCheckedChange: (value) => {
|
|
244
|
+
setTermsAccepted(value === true);
|
|
245
|
+
if (value === true) {
|
|
246
|
+
setFieldErrors((prev) => {
|
|
247
|
+
const next = { ...prev };
|
|
248
|
+
delete next.termsAccepted;
|
|
249
|
+
return next;
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
},
|
|
253
|
+
"aria-invalid": Boolean(fieldErrors.termsAccepted)
|
|
254
|
+
}
|
|
255
|
+
),
|
|
256
|
+
/* @__PURE__ */ jsxs("span", { children: [
|
|
257
|
+
translate("onboarding.form.termsLabel", "I have read and accept the terms of service"),
|
|
258
|
+
" ",
|
|
259
|
+
/* @__PURE__ */ jsx("a", { className: "underline hover:text-foreground", href: "/terms", target: "_blank", rel: "noreferrer", children: translate("onboarding.form.termsLink", "terms of service") }),
|
|
260
|
+
fieldErrors.termsAccepted && /* @__PURE__ */ jsx("span", { className: "mt-1 block text-xs text-red-600", children: fieldErrors.termsAccepted })
|
|
261
|
+
] })
|
|
262
|
+
] }),
|
|
263
|
+
/* @__PURE__ */ jsx(
|
|
264
|
+
"button",
|
|
265
|
+
{
|
|
266
|
+
type: "submit",
|
|
267
|
+
disabled,
|
|
268
|
+
className: "mt-2 h-11 rounded-md bg-foreground text-background transition hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-60",
|
|
269
|
+
children: submitting ? translate("onboarding.form.loading", "Sending...") : translate("onboarding.form.submit", "Send verification email")
|
|
270
|
+
}
|
|
271
|
+
)
|
|
272
|
+
] })
|
|
273
|
+
] })
|
|
274
|
+
] }) });
|
|
275
|
+
}
|
|
276
|
+
export {
|
|
277
|
+
OnboardingPage as default
|
|
278
|
+
};
|
|
279
|
+
//# sourceMappingURL=page.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../../src/modules/onboarding/frontend/onboarding/page.tsx"],
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport Image from 'next/image'\nimport { useState } from 'react'\nimport { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@open-mercato/ui/primitives/card'\nimport { Input } from '@open-mercato/ui/primitives/input'\nimport { Label } from '@open-mercato/ui/primitives/label'\nimport { Checkbox } from '@open-mercato/ui/primitives/checkbox'\nimport { useT, useLocale } 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 { onboardingStartSchema } from '@open-mercato/onboarding/modules/onboarding/data/validators'\n\ntype SubmissionState = 'idle' | 'loading' | 'success'\ntype FieldErrors = Partial<Record<\n 'email' | 'firstName' | 'lastName' | 'organizationName' | 'password' | 'confirmPassword' | 'termsAccepted',\n string\n>>\n\nexport default function OnboardingPage() {\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 [emailSubmitted, setEmailSubmitted] = useState<string | null>(null)\n\n async function onSubmit(event: React.FormEvent<HTMLFormElement>) {\n event.preventDefault()\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: termsAccepted,\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('onboarding.errors.passwordRequired', 'Password must be at least 6 characters.')\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 }),\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 submitting = state === 'loading'\n const disabled = submitting || state === 'success'\n\n return (\n <div className=\"relative min-h-svh flex items-center justify-center bg-muted/40 px-4 pb-24\">\n <Card className=\"w-full max-w-lg shadow-lg\">\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=\"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 <Input\n id=\"email\"\n name=\"email\"\n type=\"email\"\n required\n disabled={disabled}\n autoComplete=\"email\"\n aria-invalid={Boolean(fieldErrors.email)}\n aria-describedby={fieldErrors.email ? 'email-error' : undefined}\n className={fieldErrors.email ? 'border-red-500 focus-visible:ring-red-500' : 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 focus-visible:ring-red-500' : 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 focus-visible:ring-red-500' : 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 focus-visible:ring-red-500' : 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 <Input\n id=\"password\"\n name=\"password\"\n type=\"password\"\n required\n disabled={disabled}\n autoComplete=\"new-password\"\n aria-invalid={Boolean(fieldErrors.password)}\n aria-describedby={fieldErrors.password ? 'password-error' : undefined}\n className={fieldErrors.password ? 'border-red-500 focus-visible:ring-red-500' : undefined}\n />\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 <Input\n id=\"confirmPassword\"\n name=\"confirmPassword\"\n type=\"password\"\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 focus-visible:ring-red-500' : 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 terms of service')}{' '}\n <a className=\"underline hover:text-foreground\" href=\"/terms\" target=\"_blank\" rel=\"noreferrer\">\n {translate('onboarding.form.termsLink', 'terms of service')}\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 <button\n type=\"submit\"\n disabled={disabled}\n className=\"mt-2 h-11 rounded-md bg-foreground text-background transition hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-60\"\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
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
const metadata = {
|
|
2
|
+
name: "onboarding",
|
|
3
|
+
title: "Onboarding",
|
|
4
|
+
version: "0.1.0",
|
|
5
|
+
description: "Self-service tenant and organization onboarding flow.",
|
|
6
|
+
author: "Open Mercato Team",
|
|
7
|
+
license: "Proprietary"
|
|
8
|
+
};
|
|
9
|
+
import { features } from "./acl.js";
|
|
10
|
+
export {
|
|
11
|
+
features,
|
|
12
|
+
metadata
|
|
13
|
+
};
|
|
14
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/modules/onboarding/index.ts"],
|
|
4
|
+
"sourcesContent": ["import type { ModuleInfo } from '@open-mercato/shared/modules/registry'\n\nexport const metadata: ModuleInfo = {\n name: 'onboarding',\n title: 'Onboarding',\n version: '0.1.0',\n description: 'Self-service tenant and organization onboarding flow.',\n author: 'Open Mercato Team',\n license: 'Proprietary',\n}\n\nexport { features } from './acl'\n"],
|
|
5
|
+
"mappings": "AAEO,MAAM,WAAuB;AAAA,EAClC,MAAM;AAAA,EACN,OAAO;AAAA,EACP,SAAS;AAAA,EACT,aAAa;AAAA,EACb,QAAQ;AAAA,EACR,SAAS;AACX;AAEA,SAAS,gBAAgB;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { randomBytes, createHash } from "node:crypto";
|
|
2
|
+
import { hash } from "bcryptjs";
|
|
3
|
+
import { OnboardingRequest } from "../data/entities.js";
|
|
4
|
+
class OnboardingService {
|
|
5
|
+
constructor(em) {
|
|
6
|
+
this.em = em;
|
|
7
|
+
}
|
|
8
|
+
async createOrUpdateRequest(input, options = {}) {
|
|
9
|
+
const expiresInHours = options.expiresInHours ?? 24;
|
|
10
|
+
const token = randomBytes(32).toString("hex");
|
|
11
|
+
const tokenHash = hashToken(token);
|
|
12
|
+
const expiresAt = new Date(Date.now() + expiresInHours * 60 * 60 * 1e3);
|
|
13
|
+
const now = /* @__PURE__ */ new Date();
|
|
14
|
+
const passwordHash = await hash(input.password, 10);
|
|
15
|
+
const existing = await this.em.findOne(OnboardingRequest, { email: input.email });
|
|
16
|
+
if (existing) {
|
|
17
|
+
const lastSentAt = existing.lastEmailSentAt ?? existing.updatedAt ?? existing.createdAt;
|
|
18
|
+
if (existing.status === "pending" && lastSentAt && lastSentAt.getTime() > Date.now() - 10 * 60 * 1e3) {
|
|
19
|
+
const remainingMs = 10 * 60 * 1e3 - (Date.now() - lastSentAt.getTime());
|
|
20
|
+
const waitMinutes = Math.max(1, Math.ceil(remainingMs / (60 * 1e3)));
|
|
21
|
+
throw new Error(`PENDING_REQUEST:${waitMinutes}`);
|
|
22
|
+
}
|
|
23
|
+
existing.tokenHash = tokenHash;
|
|
24
|
+
existing.status = "pending";
|
|
25
|
+
existing.firstName = input.firstName;
|
|
26
|
+
existing.lastName = input.lastName;
|
|
27
|
+
existing.organizationName = input.organizationName;
|
|
28
|
+
existing.locale = input.locale ?? existing.locale ?? "en";
|
|
29
|
+
existing.termsAccepted = true;
|
|
30
|
+
existing.passwordHash = passwordHash;
|
|
31
|
+
existing.expiresAt = expiresAt;
|
|
32
|
+
existing.completedAt = null;
|
|
33
|
+
existing.tenantId = null;
|
|
34
|
+
existing.organizationId = null;
|
|
35
|
+
existing.userId = null;
|
|
36
|
+
existing.lastEmailSentAt = now;
|
|
37
|
+
await this.em.flush();
|
|
38
|
+
return { request: existing, token };
|
|
39
|
+
}
|
|
40
|
+
const request = this.em.create(OnboardingRequest, {
|
|
41
|
+
email: input.email,
|
|
42
|
+
tokenHash,
|
|
43
|
+
status: "pending",
|
|
44
|
+
firstName: input.firstName,
|
|
45
|
+
lastName: input.lastName,
|
|
46
|
+
organizationName: input.organizationName,
|
|
47
|
+
locale: input.locale ?? "en",
|
|
48
|
+
termsAccepted: true,
|
|
49
|
+
passwordHash,
|
|
50
|
+
expiresAt,
|
|
51
|
+
lastEmailSentAt: now,
|
|
52
|
+
createdAt: now,
|
|
53
|
+
updatedAt: now
|
|
54
|
+
});
|
|
55
|
+
await this.em.persistAndFlush(request);
|
|
56
|
+
return { request, token };
|
|
57
|
+
}
|
|
58
|
+
async findPendingByToken(token) {
|
|
59
|
+
const tokenHash = hashToken(token);
|
|
60
|
+
const now = /* @__PURE__ */ new Date();
|
|
61
|
+
return this.em.findOne(OnboardingRequest, {
|
|
62
|
+
tokenHash,
|
|
63
|
+
status: "pending",
|
|
64
|
+
expiresAt: { $gt: now }
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
async markCompleted(request, data) {
|
|
68
|
+
request.status = "completed";
|
|
69
|
+
request.completedAt = /* @__PURE__ */ new Date();
|
|
70
|
+
request.tenantId = data.tenantId;
|
|
71
|
+
request.organizationId = data.organizationId;
|
|
72
|
+
request.userId = data.userId;
|
|
73
|
+
request.passwordHash = null;
|
|
74
|
+
await this.em.flush();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function hashToken(token) {
|
|
78
|
+
return createHash("sha256").update(token).digest("hex");
|
|
79
|
+
}
|
|
80
|
+
export {
|
|
81
|
+
OnboardingService
|
|
82
|
+
};
|
|
83
|
+
//# sourceMappingURL=service.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/onboarding/lib/service.ts"],
|
|
4
|
+
"sourcesContent": ["import { randomBytes, createHash } from 'node:crypto'\nimport { hash } from 'bcryptjs'\nimport { EntityManager } from '@mikro-orm/postgresql'\nimport { OnboardingRequest } from '../data/entities'\nimport type { OnboardingStartInput } from '../data/validators'\n\ntype CreateRequestOptions = {\n expiresInHours?: number\n}\n\nexport class OnboardingService {\n constructor(private readonly em: EntityManager) {}\n\n async createOrUpdateRequest(input: OnboardingStartInput, options: CreateRequestOptions = {}) {\n const expiresInHours = options.expiresInHours ?? 24\n const token = randomBytes(32).toString('hex')\n const tokenHash = hashToken(token)\n const expiresAt = new Date(Date.now() + expiresInHours * 60 * 60 * 1000)\n const now = new Date()\n const passwordHash = await hash(input.password, 10)\n\n const existing = await this.em.findOne(OnboardingRequest, { email: input.email })\n if (existing) {\n const lastSentAt = existing.lastEmailSentAt ?? existing.updatedAt ?? existing.createdAt\n if (existing.status === 'pending' && lastSentAt && lastSentAt.getTime() > Date.now() - 10 * 60 * 1000) {\n const remainingMs = 10 * 60 * 1000 - (Date.now() - lastSentAt.getTime())\n const waitMinutes = Math.max(1, Math.ceil(remainingMs / (60 * 1000)))\n throw new Error(`PENDING_REQUEST:${waitMinutes}`)\n }\n existing.tokenHash = tokenHash\n existing.status = 'pending'\n existing.firstName = input.firstName\n existing.lastName = input.lastName\n existing.organizationName = input.organizationName\n existing.locale = input.locale ?? existing.locale ?? 'en'\n existing.termsAccepted = true\n existing.passwordHash = passwordHash\n existing.expiresAt = expiresAt\n existing.completedAt = null\n existing.tenantId = null\n existing.organizationId = null\n existing.userId = null\n existing.lastEmailSentAt = now\n await this.em.flush()\n return { request: existing, token }\n }\n\n const request = this.em.create(OnboardingRequest, {\n email: input.email,\n tokenHash,\n status: 'pending',\n firstName: input.firstName,\n lastName: input.lastName,\n organizationName: input.organizationName,\n locale: input.locale ?? 'en',\n termsAccepted: true,\n passwordHash,\n expiresAt,\n lastEmailSentAt: now,\n createdAt: now,\n updatedAt: now,\n })\n await this.em.persistAndFlush(request)\n return { request, token }\n }\n\n async findPendingByToken(token: string) {\n const tokenHash = hashToken(token)\n const now = new Date()\n return this.em.findOne(OnboardingRequest, {\n tokenHash,\n status: 'pending',\n expiresAt: { $gt: now } as any,\n })\n }\n\n async markCompleted(request: OnboardingRequest, data: { tenantId: string; organizationId: string; userId: string }) {\n request.status = 'completed'\n request.completedAt = new Date()\n request.tenantId = data.tenantId\n request.organizationId = data.organizationId\n request.userId = data.userId\n request.passwordHash = null\n await this.em.flush()\n }\n}\n\nfunction hashToken(token: string) {\n return createHash('sha256').update(token).digest('hex')\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,aAAa,kBAAkB;AACxC,SAAS,YAAY;AAErB,SAAS,yBAAyB;AAO3B,MAAM,kBAAkB;AAAA,EAC7B,YAA6B,IAAmB;AAAnB;AAAA,EAAoB;AAAA,EAEjD,MAAM,sBAAsB,OAA6B,UAAgC,CAAC,GAAG;AAC3F,UAAM,iBAAiB,QAAQ,kBAAkB;AACjD,UAAM,QAAQ,YAAY,EAAE,EAAE,SAAS,KAAK;AAC5C,UAAM,YAAY,UAAU,KAAK;AACjC,UAAM,YAAY,IAAI,KAAK,KAAK,IAAI,IAAI,iBAAiB,KAAK,KAAK,GAAI;AACvE,UAAM,MAAM,oBAAI,KAAK;AACrB,UAAM,eAAe,MAAM,KAAK,MAAM,UAAU,EAAE;AAElD,UAAM,WAAW,MAAM,KAAK,GAAG,QAAQ,mBAAmB,EAAE,OAAO,MAAM,MAAM,CAAC;AAChF,QAAI,UAAU;AACZ,YAAM,aAAa,SAAS,mBAAmB,SAAS,aAAa,SAAS;AAC9E,UAAI,SAAS,WAAW,aAAa,cAAc,WAAW,QAAQ,IAAI,KAAK,IAAI,IAAI,KAAK,KAAK,KAAM;AACrG,cAAM,cAAc,KAAK,KAAK,OAAQ,KAAK,IAAI,IAAI,WAAW,QAAQ;AACtE,cAAM,cAAc,KAAK,IAAI,GAAG,KAAK,KAAK,eAAe,KAAK,IAAK,CAAC;AACpE,cAAM,IAAI,MAAM,mBAAmB,WAAW,EAAE;AAAA,MAClD;AACA,eAAS,YAAY;AACrB,eAAS,SAAS;AAClB,eAAS,YAAY,MAAM;AAC3B,eAAS,WAAW,MAAM;AAC1B,eAAS,mBAAmB,MAAM;AAClC,eAAS,SAAS,MAAM,UAAU,SAAS,UAAU;AACrD,eAAS,gBAAgB;AACzB,eAAS,eAAe;AACxB,eAAS,YAAY;AACrB,eAAS,cAAc;AACvB,eAAS,WAAW;AACpB,eAAS,iBAAiB;AAC1B,eAAS,SAAS;AAClB,eAAS,kBAAkB;AAC3B,YAAM,KAAK,GAAG,MAAM;AACpB,aAAO,EAAE,SAAS,UAAU,MAAM;AAAA,IACpC;AAEA,UAAM,UAAU,KAAK,GAAG,OAAO,mBAAmB;AAAA,MAChD,OAAO,MAAM;AAAA,MACb;AAAA,MACA,QAAQ;AAAA,MACR,WAAW,MAAM;AAAA,MACjB,UAAU,MAAM;AAAA,MAChB,kBAAkB,MAAM;AAAA,MACxB,QAAQ,MAAM,UAAU;AAAA,MACxB,eAAe;AAAA,MACf;AAAA,MACA;AAAA,MACA,iBAAiB;AAAA,MACjB,WAAW;AAAA,MACX,WAAW;AAAA,IACb,CAAC;AACD,UAAM,KAAK,GAAG,gBAAgB,OAAO;AACrC,WAAO,EAAE,SAAS,MAAM;AAAA,EAC1B;AAAA,EAEA,MAAM,mBAAmB,OAAe;AACtC,UAAM,YAAY,UAAU,KAAK;AACjC,UAAM,MAAM,oBAAI,KAAK;AACrB,WAAO,KAAK,GAAG,QAAQ,mBAAmB;AAAA,MACxC;AAAA,MACA,QAAQ;AAAA,MACR,WAAW,EAAE,KAAK,IAAI;AAAA,IACxB,CAAC;AAAA,EACH;AAAA,EAEA,MAAM,cAAc,SAA4B,MAAoE;AAClH,YAAQ,SAAS;AACjB,YAAQ,cAAc,oBAAI,KAAK;AAC/B,YAAQ,WAAW,KAAK;AACxB,YAAQ,iBAAiB,KAAK;AAC9B,YAAQ,SAAS,KAAK;AACtB,YAAQ,eAAe;AACvB,UAAM,KAAK,GAAG,MAAM;AAAA,EACtB;AACF;AAEA,SAAS,UAAU,OAAe;AAChC,SAAO,WAAW,QAAQ,EAAE,OAAO,KAAK,EAAE,OAAO,KAAK;AACxD;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Migration } from "@mikro-orm/migrations";
|
|
2
|
+
class Migration20260112142945 extends Migration {
|
|
3
|
+
async up() {
|
|
4
|
+
this.addSql(`create table "onboarding_requests" ("id" uuid not null default gen_random_uuid(), "email" text not null, "token_hash" text not null, "status" text not null default 'pending', "first_name" text not null, "last_name" text not null, "organization_name" text not null, "locale" text null, "terms_accepted" boolean not null default false, "password_hash" text null, "expires_at" timestamptz not null, "completed_at" timestamptz null, "tenant_id" uuid null, "organization_id" uuid null, "user_id" uuid null, "last_email_sent_at" timestamptz null, "created_at" timestamptz not null, "updated_at" timestamptz null, "deleted_at" timestamptz null, constraint "onboarding_requests_pkey" primary key ("id"));`);
|
|
5
|
+
this.addSql(`alter table "onboarding_requests" add constraint "onboarding_requests_token_hash_unique" unique ("token_hash");`);
|
|
6
|
+
this.addSql(`alter table "onboarding_requests" add constraint "onboarding_requests_email_unique" unique ("email");`);
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
export {
|
|
10
|
+
Migration20260112142945
|
|
11
|
+
};
|
|
12
|
+
//# sourceMappingURL=Migration20260112142945.js.map
|