@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,90 @@
1
+ import { randomBytes, createHash } from 'node:crypto'
2
+ import { hash } from 'bcryptjs'
3
+ import { EntityManager } from '@mikro-orm/postgresql'
4
+ import { OnboardingRequest } from '../data/entities'
5
+ import type { OnboardingStartInput } from '../data/validators'
6
+
7
+ type CreateRequestOptions = {
8
+ expiresInHours?: number
9
+ }
10
+
11
+ export class OnboardingService {
12
+ constructor(private readonly em: EntityManager) {}
13
+
14
+ async createOrUpdateRequest(input: OnboardingStartInput, options: CreateRequestOptions = {}) {
15
+ const expiresInHours = options.expiresInHours ?? 24
16
+ const token = randomBytes(32).toString('hex')
17
+ const tokenHash = hashToken(token)
18
+ const expiresAt = new Date(Date.now() + expiresInHours * 60 * 60 * 1000)
19
+ const now = new Date()
20
+ const passwordHash = await hash(input.password, 10)
21
+
22
+ const existing = await this.em.findOne(OnboardingRequest, { email: input.email })
23
+ if (existing) {
24
+ const lastSentAt = existing.lastEmailSentAt ?? existing.updatedAt ?? existing.createdAt
25
+ if (existing.status === 'pending' && lastSentAt && lastSentAt.getTime() > Date.now() - 10 * 60 * 1000) {
26
+ const remainingMs = 10 * 60 * 1000 - (Date.now() - lastSentAt.getTime())
27
+ const waitMinutes = Math.max(1, Math.ceil(remainingMs / (60 * 1000)))
28
+ throw new Error(`PENDING_REQUEST:${waitMinutes}`)
29
+ }
30
+ existing.tokenHash = tokenHash
31
+ existing.status = 'pending'
32
+ existing.firstName = input.firstName
33
+ existing.lastName = input.lastName
34
+ existing.organizationName = input.organizationName
35
+ existing.locale = input.locale ?? existing.locale ?? 'en'
36
+ existing.termsAccepted = true
37
+ existing.passwordHash = passwordHash
38
+ existing.expiresAt = expiresAt
39
+ existing.completedAt = null
40
+ existing.tenantId = null
41
+ existing.organizationId = null
42
+ existing.userId = null
43
+ existing.lastEmailSentAt = now
44
+ await this.em.flush()
45
+ return { request: existing, token }
46
+ }
47
+
48
+ const request = this.em.create(OnboardingRequest, {
49
+ email: input.email,
50
+ tokenHash,
51
+ status: 'pending',
52
+ firstName: input.firstName,
53
+ lastName: input.lastName,
54
+ organizationName: input.organizationName,
55
+ locale: input.locale ?? 'en',
56
+ termsAccepted: true,
57
+ passwordHash,
58
+ expiresAt,
59
+ lastEmailSentAt: now,
60
+ createdAt: now,
61
+ updatedAt: now,
62
+ })
63
+ await this.em.persistAndFlush(request)
64
+ return { request, token }
65
+ }
66
+
67
+ async findPendingByToken(token: string) {
68
+ const tokenHash = hashToken(token)
69
+ const now = new Date()
70
+ return this.em.findOne(OnboardingRequest, {
71
+ tokenHash,
72
+ status: 'pending',
73
+ expiresAt: { $gt: now } as any,
74
+ })
75
+ }
76
+
77
+ async markCompleted(request: OnboardingRequest, data: { tenantId: string; organizationId: string; userId: string }) {
78
+ request.status = 'completed'
79
+ request.completedAt = new Date()
80
+ request.tenantId = data.tenantId
81
+ request.organizationId = data.organizationId
82
+ request.userId = data.userId
83
+ request.passwordHash = null
84
+ await this.em.flush()
85
+ }
86
+ }
87
+
88
+ function hashToken(token: string) {
89
+ return createHash('sha256').update(token).digest('hex')
90
+ }
@@ -0,0 +1,230 @@
1
+ {
2
+ "namespaces": [
3
+ "public"
4
+ ],
5
+ "name": "public",
6
+ "tables": [
7
+ {
8
+ "columns": {
9
+ "id": {
10
+ "name": "id",
11
+ "type": "uuid",
12
+ "unsigned": false,
13
+ "autoincrement": false,
14
+ "primary": false,
15
+ "nullable": false,
16
+ "default": "gen_random_uuid()",
17
+ "mappedType": "uuid"
18
+ },
19
+ "email": {
20
+ "name": "email",
21
+ "type": "text",
22
+ "unsigned": false,
23
+ "autoincrement": false,
24
+ "primary": false,
25
+ "nullable": false,
26
+ "mappedType": "text"
27
+ },
28
+ "token_hash": {
29
+ "name": "token_hash",
30
+ "type": "text",
31
+ "unsigned": false,
32
+ "autoincrement": false,
33
+ "primary": false,
34
+ "nullable": false,
35
+ "mappedType": "text"
36
+ },
37
+ "status": {
38
+ "name": "status",
39
+ "type": "text",
40
+ "unsigned": false,
41
+ "autoincrement": false,
42
+ "primary": false,
43
+ "nullable": false,
44
+ "default": "'pending'",
45
+ "mappedType": "text"
46
+ },
47
+ "first_name": {
48
+ "name": "first_name",
49
+ "type": "text",
50
+ "unsigned": false,
51
+ "autoincrement": false,
52
+ "primary": false,
53
+ "nullable": false,
54
+ "mappedType": "text"
55
+ },
56
+ "last_name": {
57
+ "name": "last_name",
58
+ "type": "text",
59
+ "unsigned": false,
60
+ "autoincrement": false,
61
+ "primary": false,
62
+ "nullable": false,
63
+ "mappedType": "text"
64
+ },
65
+ "organization_name": {
66
+ "name": "organization_name",
67
+ "type": "text",
68
+ "unsigned": false,
69
+ "autoincrement": false,
70
+ "primary": false,
71
+ "nullable": false,
72
+ "mappedType": "text"
73
+ },
74
+ "locale": {
75
+ "name": "locale",
76
+ "type": "text",
77
+ "unsigned": false,
78
+ "autoincrement": false,
79
+ "primary": false,
80
+ "nullable": true,
81
+ "mappedType": "text"
82
+ },
83
+ "terms_accepted": {
84
+ "name": "terms_accepted",
85
+ "type": "boolean",
86
+ "unsigned": false,
87
+ "autoincrement": false,
88
+ "primary": false,
89
+ "nullable": false,
90
+ "default": "false",
91
+ "mappedType": "boolean"
92
+ },
93
+ "password_hash": {
94
+ "name": "password_hash",
95
+ "type": "text",
96
+ "unsigned": false,
97
+ "autoincrement": false,
98
+ "primary": false,
99
+ "nullable": true,
100
+ "mappedType": "text"
101
+ },
102
+ "expires_at": {
103
+ "name": "expires_at",
104
+ "type": "timestamptz",
105
+ "unsigned": false,
106
+ "autoincrement": false,
107
+ "primary": false,
108
+ "nullable": false,
109
+ "length": 6,
110
+ "mappedType": "datetime"
111
+ },
112
+ "completed_at": {
113
+ "name": "completed_at",
114
+ "type": "timestamptz",
115
+ "unsigned": false,
116
+ "autoincrement": false,
117
+ "primary": false,
118
+ "nullable": true,
119
+ "length": 6,
120
+ "mappedType": "datetime"
121
+ },
122
+ "tenant_id": {
123
+ "name": "tenant_id",
124
+ "type": "uuid",
125
+ "unsigned": false,
126
+ "autoincrement": false,
127
+ "primary": false,
128
+ "nullable": true,
129
+ "mappedType": "uuid"
130
+ },
131
+ "organization_id": {
132
+ "name": "organization_id",
133
+ "type": "uuid",
134
+ "unsigned": false,
135
+ "autoincrement": false,
136
+ "primary": false,
137
+ "nullable": true,
138
+ "mappedType": "uuid"
139
+ },
140
+ "user_id": {
141
+ "name": "user_id",
142
+ "type": "uuid",
143
+ "unsigned": false,
144
+ "autoincrement": false,
145
+ "primary": false,
146
+ "nullable": true,
147
+ "mappedType": "uuid"
148
+ },
149
+ "last_email_sent_at": {
150
+ "name": "last_email_sent_at",
151
+ "type": "timestamptz",
152
+ "unsigned": false,
153
+ "autoincrement": false,
154
+ "primary": false,
155
+ "nullable": true,
156
+ "length": 6,
157
+ "mappedType": "datetime"
158
+ },
159
+ "created_at": {
160
+ "name": "created_at",
161
+ "type": "timestamptz",
162
+ "unsigned": false,
163
+ "autoincrement": false,
164
+ "primary": false,
165
+ "nullable": false,
166
+ "length": 6,
167
+ "mappedType": "datetime"
168
+ },
169
+ "updated_at": {
170
+ "name": "updated_at",
171
+ "type": "timestamptz",
172
+ "unsigned": false,
173
+ "autoincrement": false,
174
+ "primary": false,
175
+ "nullable": true,
176
+ "length": 6,
177
+ "mappedType": "datetime"
178
+ },
179
+ "deleted_at": {
180
+ "name": "deleted_at",
181
+ "type": "timestamptz",
182
+ "unsigned": false,
183
+ "autoincrement": false,
184
+ "primary": false,
185
+ "nullable": true,
186
+ "length": 6,
187
+ "mappedType": "datetime"
188
+ }
189
+ },
190
+ "name": "onboarding_requests",
191
+ "schema": "public",
192
+ "indexes": [
193
+ {
194
+ "keyName": "onboarding_requests_token_hash_unique",
195
+ "columnNames": [
196
+ "token_hash"
197
+ ],
198
+ "composite": false,
199
+ "constraint": true,
200
+ "primary": false,
201
+ "unique": true
202
+ },
203
+ {
204
+ "keyName": "onboarding_requests_email_unique",
205
+ "columnNames": [
206
+ "email"
207
+ ],
208
+ "composite": false,
209
+ "constraint": true,
210
+ "primary": false,
211
+ "unique": true
212
+ },
213
+ {
214
+ "keyName": "onboarding_requests_pkey",
215
+ "columnNames": [
216
+ "id"
217
+ ],
218
+ "composite": false,
219
+ "constraint": true,
220
+ "primary": true,
221
+ "unique": true
222
+ }
223
+ ],
224
+ "checks": [],
225
+ "foreignKeys": {},
226
+ "nativeEnums": {}
227
+ }
228
+ ],
229
+ "nativeEnums": {}
230
+ }
@@ -0,0 +1,11 @@
1
+ import { Migration } from '@mikro-orm/migrations';
2
+
3
+ export class Migration20260112142945 extends Migration {
4
+
5
+ override async up(): Promise<void> {
6
+ 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"));`);
7
+ this.addSql(`alter table "onboarding_requests" add constraint "onboarding_requests_token_hash_unique" unique ("token_hash");`);
8
+ this.addSql(`alter table "onboarding_requests" add constraint "onboarding_requests_email_unique" unique ("email");`);
9
+ }
10
+
11
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/tsconfig",
3
+ "extends": "./tsconfig.json"
4
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/tsconfig",
3
+ "extends": "../../tsconfig.base.json",
4
+ "compilerOptions": {
5
+ "noEmit": true
6
+ },
7
+ "include": ["src/**/*", "generated/**/*"],
8
+ "exclude": ["node_modules", "dist", "**/__tests__/**"]
9
+ }
package/watch.mjs ADDED
@@ -0,0 +1,6 @@
1
+ import { watch } from '../../scripts/watch.mjs'
2
+ import { dirname } from 'node:path'
3
+ import { fileURLToPath } from 'node:url'
4
+
5
+ const __dirname = dirname(fileURLToPath(import.meta.url))
6
+ watch(__dirname)