@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.
Files changed (50) hide show
  1. package/build.mjs +62 -0
  2. package/dist/index.js +1 -0
  3. package/dist/index.js.map +7 -0
  4. package/dist/modules/onboarding/acl.js +11 -0
  5. package/dist/modules/onboarding/acl.js.map +7 -0
  6. package/dist/modules/onboarding/api/get/onboarding/verify.js +208 -0
  7. package/dist/modules/onboarding/api/get/onboarding/verify.js.map +7 -0
  8. package/dist/modules/onboarding/api/post/onboarding.js +193 -0
  9. package/dist/modules/onboarding/api/post/onboarding.js.map +7 -0
  10. package/dist/modules/onboarding/data/entities.js +84 -0
  11. package/dist/modules/onboarding/data/entities.js.map +7 -0
  12. package/dist/modules/onboarding/data/validators.js +27 -0
  13. package/dist/modules/onboarding/data/validators.js.map +7 -0
  14. package/dist/modules/onboarding/emails/AdminNotificationEmail.js +18 -0
  15. package/dist/modules/onboarding/emails/AdminNotificationEmail.js.map +7 -0
  16. package/dist/modules/onboarding/emails/VerificationEmail.js +36 -0
  17. package/dist/modules/onboarding/emails/VerificationEmail.js.map +7 -0
  18. package/dist/modules/onboarding/frontend/onboarding/page.js +279 -0
  19. package/dist/modules/onboarding/frontend/onboarding/page.js.map +7 -0
  20. package/dist/modules/onboarding/index.js +14 -0
  21. package/dist/modules/onboarding/index.js.map +7 -0
  22. package/dist/modules/onboarding/lib/service.js +83 -0
  23. package/dist/modules/onboarding/lib/service.js.map +7 -0
  24. package/dist/modules/onboarding/migrations/Migration20260112142945.js +12 -0
  25. package/dist/modules/onboarding/migrations/Migration20260112142945.js.map +7 -0
  26. package/generated/entities/onboarding_request/index.ts +19 -0
  27. package/generated/entities.ids.generated.ts +11 -0
  28. package/generated/entity-fields-registry.ts +11 -0
  29. package/jest.config.cjs +19 -0
  30. package/package.json +83 -0
  31. package/src/index.ts +2 -0
  32. package/src/modules/onboarding/acl.ts +7 -0
  33. package/src/modules/onboarding/api/get/onboarding/verify.ts +224 -0
  34. package/src/modules/onboarding/api/post/onboarding.ts +210 -0
  35. package/src/modules/onboarding/data/entities.ts +67 -0
  36. package/src/modules/onboarding/data/validators.ts +27 -0
  37. package/src/modules/onboarding/emails/AdminNotificationEmail.tsx +32 -0
  38. package/src/modules/onboarding/emails/VerificationEmail.tsx +54 -0
  39. package/src/modules/onboarding/frontend/onboarding/page.tsx +305 -0
  40. package/src/modules/onboarding/i18n/de.json +49 -0
  41. package/src/modules/onboarding/i18n/en.json +49 -0
  42. package/src/modules/onboarding/i18n/es.json +49 -0
  43. package/src/modules/onboarding/i18n/pl.json +49 -0
  44. package/src/modules/onboarding/index.ts +12 -0
  45. package/src/modules/onboarding/lib/service.ts +90 -0
  46. package/src/modules/onboarding/migrations/.snapshot-open-mercato.json +230 -0
  47. package/src/modules/onboarding/migrations/Migration20260112142945.ts +11 -0
  48. package/tsconfig.build.json +4 -0
  49. package/tsconfig.json +9 -0
  50. 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