@open-mercato/core 0.4.8-develop-28cee031d6 → 0.4.8-develop-84f3678a58

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 (330) hide show
  1. package/agentic/standalone-guide.md +235 -0
  2. package/dist/generated/entities/customer_role/index.js +27 -0
  3. package/dist/generated/entities/customer_role/index.js.map +7 -0
  4. package/dist/generated/entities/customer_role_acl/index.js +19 -0
  5. package/dist/generated/entities/customer_role_acl/index.js.map +7 -0
  6. package/dist/generated/entities/customer_user/index.js +37 -0
  7. package/dist/generated/entities/customer_user/index.js.map +7 -0
  8. package/dist/generated/entities/customer_user_acl/index.js +19 -0
  9. package/dist/generated/entities/customer_user_acl/index.js.map +7 -0
  10. package/dist/generated/entities/customer_user_email_verification/index.js +17 -0
  11. package/dist/generated/entities/customer_user_email_verification/index.js.map +7 -0
  12. package/dist/generated/entities/customer_user_invitation/index.js +33 -0
  13. package/dist/generated/entities/customer_user_invitation/index.js.map +7 -0
  14. package/dist/generated/entities/customer_user_password_reset/index.js +15 -0
  15. package/dist/generated/entities/customer_user_password_reset/index.js.map +7 -0
  16. package/dist/generated/entities/customer_user_role/index.js +13 -0
  17. package/dist/generated/entities/customer_user_role/index.js.map +7 -0
  18. package/dist/generated/entities/customer_user_session/index.js +21 -0
  19. package/dist/generated/entities/customer_user_session/index.js.map +7 -0
  20. package/dist/generated/entities/organization/index.js +2 -0
  21. package/dist/generated/entities/organization/index.js.map +2 -2
  22. package/dist/generated/entities.ids.generated.js +14 -1
  23. package/dist/generated/entities.ids.generated.js.map +2 -2
  24. package/dist/generated/entity-fields-registry.js +18 -0
  25. package/dist/generated/entity-fields-registry.js.map +2 -2
  26. package/dist/modules/auth/services/rbacService.js +3 -9
  27. package/dist/modules/auth/services/rbacService.js.map +2 -2
  28. package/dist/modules/customer_accounts/acl.js +12 -0
  29. package/dist/modules/customer_accounts/acl.js.map +7 -0
  30. package/dist/modules/customer_accounts/api/admin/roles/[id]/acl.js +87 -0
  31. package/dist/modules/customer_accounts/api/admin/roles/[id]/acl.js.map +7 -0
  32. package/dist/modules/customer_accounts/api/admin/roles/[id].js +216 -0
  33. package/dist/modules/customer_accounts/api/admin/roles/[id].js.map +7 -0
  34. package/dist/modules/customer_accounts/api/admin/roles.js +189 -0
  35. package/dist/modules/customer_accounts/api/admin/roles.js.map +7 -0
  36. package/dist/modules/customer_accounts/api/admin/users/[id]/reset-password.js +69 -0
  37. package/dist/modules/customer_accounts/api/admin/users/[id]/reset-password.js.map +7 -0
  38. package/dist/modules/customer_accounts/api/admin/users/[id]/verify-email.js +64 -0
  39. package/dist/modules/customer_accounts/api/admin/users/[id]/verify-email.js.map +7 -0
  40. package/dist/modules/customer_accounts/api/admin/users/[id].js +253 -0
  41. package/dist/modules/customer_accounts/api/admin/users/[id].js.map +7 -0
  42. package/dist/modules/customer_accounts/api/admin/users-invite.js +78 -0
  43. package/dist/modules/customer_accounts/api/admin/users-invite.js.map +7 -0
  44. package/dist/modules/customer_accounts/api/admin/users.js +251 -0
  45. package/dist/modules/customer_accounts/api/admin/users.js.map +7 -0
  46. package/dist/modules/customer_accounts/api/email/verify.js +59 -0
  47. package/dist/modules/customer_accounts/api/email/verify.js.map +7 -0
  48. package/dist/modules/customer_accounts/api/interceptors.js +5 -0
  49. package/dist/modules/customer_accounts/api/interceptors.js.map +7 -0
  50. package/dist/modules/customer_accounts/api/invitations/accept.js +114 -0
  51. package/dist/modules/customer_accounts/api/invitations/accept.js.map +7 -0
  52. package/dist/modules/customer_accounts/api/login.js +143 -0
  53. package/dist/modules/customer_accounts/api/login.js.map +7 -0
  54. package/dist/modules/customer_accounts/api/magic-link/request.js +78 -0
  55. package/dist/modules/customer_accounts/api/magic-link/request.js.map +7 -0
  56. package/dist/modules/customer_accounts/api/magic-link/verify.js +114 -0
  57. package/dist/modules/customer_accounts/api/magic-link/verify.js.map +7 -0
  58. package/dist/modules/customer_accounts/api/password/reset-confirm.js +59 -0
  59. package/dist/modules/customer_accounts/api/password/reset-confirm.js.map +7 -0
  60. package/dist/modules/customer_accounts/api/password/reset-request.js +77 -0
  61. package/dist/modules/customer_accounts/api/password/reset-request.js.map +7 -0
  62. package/dist/modules/customer_accounts/api/portal/events/stream.js +163 -0
  63. package/dist/modules/customer_accounts/api/portal/events/stream.js.map +7 -0
  64. package/dist/modules/customer_accounts/api/portal/feature-check.js +57 -0
  65. package/dist/modules/customer_accounts/api/portal/feature-check.js.map +7 -0
  66. package/dist/modules/customer_accounts/api/portal/logout.js +64 -0
  67. package/dist/modules/customer_accounts/api/portal/logout.js.map +7 -0
  68. package/dist/modules/customer_accounts/api/portal/notifications/[id]/dismiss.js +49 -0
  69. package/dist/modules/customer_accounts/api/portal/notifications/[id]/dismiss.js.map +7 -0
  70. package/dist/modules/customer_accounts/api/portal/notifications/[id]/read.js +49 -0
  71. package/dist/modules/customer_accounts/api/portal/notifications/[id]/read.js.map +7 -0
  72. package/dist/modules/customer_accounts/api/portal/notifications/mark-all-read.js +46 -0
  73. package/dist/modules/customer_accounts/api/portal/notifications/mark-all-read.js.map +7 -0
  74. package/dist/modules/customer_accounts/api/portal/notifications/unread-count.js +42 -0
  75. package/dist/modules/customer_accounts/api/portal/notifications/unread-count.js.map +7 -0
  76. package/dist/modules/customer_accounts/api/portal/notifications.js +105 -0
  77. package/dist/modules/customer_accounts/api/portal/notifications.js.map +7 -0
  78. package/dist/modules/customer_accounts/api/portal/password-change.js +57 -0
  79. package/dist/modules/customer_accounts/api/portal/password-change.js.map +7 -0
  80. package/dist/modules/customer_accounts/api/portal/profile.js +135 -0
  81. package/dist/modules/customer_accounts/api/portal/profile.js.map +7 -0
  82. package/dist/modules/customer_accounts/api/portal/sessions/[id].js +62 -0
  83. package/dist/modules/customer_accounts/api/portal/sessions/[id].js.map +7 -0
  84. package/dist/modules/customer_accounts/api/portal/sessions-refresh.js +75 -0
  85. package/dist/modules/customer_accounts/api/portal/sessions-refresh.js.map +7 -0
  86. package/dist/modules/customer_accounts/api/portal/sessions.js +77 -0
  87. package/dist/modules/customer_accounts/api/portal/sessions.js.map +7 -0
  88. package/dist/modules/customer_accounts/api/portal/users/[id]/roles.js +90 -0
  89. package/dist/modules/customer_accounts/api/portal/users/[id]/roles.js.map +7 -0
  90. package/dist/modules/customer_accounts/api/portal/users/[id].js +71 -0
  91. package/dist/modules/customer_accounts/api/portal/users/[id].js.map +7 -0
  92. package/dist/modules/customer_accounts/api/portal/users-invite.js +92 -0
  93. package/dist/modules/customer_accounts/api/portal/users-invite.js.map +7 -0
  94. package/dist/modules/customer_accounts/api/portal/users.js +79 -0
  95. package/dist/modules/customer_accounts/api/portal/users.js.map +7 -0
  96. package/dist/modules/customer_accounts/api/signup.js +121 -0
  97. package/dist/modules/customer_accounts/api/signup.js.map +7 -0
  98. package/dist/modules/customer_accounts/backend/customer_accounts/[id]/page.js +491 -0
  99. package/dist/modules/customer_accounts/backend/customer_accounts/[id]/page.js.map +7 -0
  100. package/dist/modules/customer_accounts/backend/customer_accounts/[id]/page.meta.js +15 -0
  101. package/dist/modules/customer_accounts/backend/customer_accounts/[id]/page.meta.js.map +7 -0
  102. package/dist/modules/customer_accounts/backend/customer_accounts/roles/[id]/page.js +343 -0
  103. package/dist/modules/customer_accounts/backend/customer_accounts/roles/[id]/page.js.map +7 -0
  104. package/dist/modules/customer_accounts/backend/customer_accounts/roles/[id]/page.meta.js +16 -0
  105. package/dist/modules/customer_accounts/backend/customer_accounts/roles/[id]/page.meta.js.map +7 -0
  106. package/dist/modules/customer_accounts/backend/customer_accounts/roles/create/page.js +180 -0
  107. package/dist/modules/customer_accounts/backend/customer_accounts/roles/create/page.js.map +7 -0
  108. package/dist/modules/customer_accounts/backend/customer_accounts/roles/create/page.meta.js +16 -0
  109. package/dist/modules/customer_accounts/backend/customer_accounts/roles/create/page.meta.js.map +7 -0
  110. package/dist/modules/customer_accounts/backend/customer_accounts/roles/page.js +176 -0
  111. package/dist/modules/customer_accounts/backend/customer_accounts/roles/page.js.map +7 -0
  112. package/dist/modules/customer_accounts/backend/customer_accounts/roles/page.meta.js +33 -0
  113. package/dist/modules/customer_accounts/backend/customer_accounts/roles/page.meta.js.map +7 -0
  114. package/dist/modules/customer_accounts/backend/page.js +466 -0
  115. package/dist/modules/customer_accounts/backend/page.js.map +7 -0
  116. package/dist/modules/customer_accounts/backend/page.meta.js +35 -0
  117. package/dist/modules/customer_accounts/backend/page.meta.js.map +7 -0
  118. package/dist/modules/customer_accounts/ce.js +26 -0
  119. package/dist/modules/customer_accounts/ce.js.map +7 -0
  120. package/dist/modules/customer_accounts/data/enrichers.js +85 -0
  121. package/dist/modules/customer_accounts/data/enrichers.js.map +7 -0
  122. package/dist/modules/customer_accounts/data/entities.js +377 -0
  123. package/dist/modules/customer_accounts/data/entities.js.map +7 -0
  124. package/dist/modules/customer_accounts/data/extensions.js +8 -0
  125. package/dist/modules/customer_accounts/data/extensions.js.map +7 -0
  126. package/dist/modules/customer_accounts/data/validators.js +111 -0
  127. package/dist/modules/customer_accounts/data/validators.js.map +7 -0
  128. package/dist/modules/customer_accounts/di.js +17 -0
  129. package/dist/modules/customer_accounts/di.js.map +7 -0
  130. package/dist/modules/customer_accounts/events.js +28 -0
  131. package/dist/modules/customer_accounts/events.js.map +7 -0
  132. package/dist/modules/customer_accounts/index.js +15 -0
  133. package/dist/modules/customer_accounts/index.js.map +7 -0
  134. package/dist/modules/customer_accounts/lib/customerAuth.js +71 -0
  135. package/dist/modules/customer_accounts/lib/customerAuth.js.map +7 -0
  136. package/dist/modules/customer_accounts/lib/customerAuthServer.js +29 -0
  137. package/dist/modules/customer_accounts/lib/customerAuthServer.js.map +7 -0
  138. package/dist/modules/customer_accounts/lib/rateLimiter.js +63 -0
  139. package/dist/modules/customer_accounts/lib/rateLimiter.js.map +7 -0
  140. package/dist/modules/customer_accounts/lib/tokenGenerator.js +12 -0
  141. package/dist/modules/customer_accounts/lib/tokenGenerator.js.map +7 -0
  142. package/dist/modules/customer_accounts/migrations/Migration20260313222043.js +49 -0
  143. package/dist/modules/customer_accounts/migrations/Migration20260313222043.js.map +7 -0
  144. package/dist/modules/customer_accounts/notifications.client.js +47 -0
  145. package/dist/modules/customer_accounts/notifications.client.js.map +7 -0
  146. package/dist/modules/customer_accounts/notifications.js +46 -0
  147. package/dist/modules/customer_accounts/notifications.js.map +7 -0
  148. package/dist/modules/customer_accounts/search.js +120 -0
  149. package/dist/modules/customer_accounts/search.js.map +7 -0
  150. package/dist/modules/customer_accounts/services/customerInvitationService.js +87 -0
  151. package/dist/modules/customer_accounts/services/customerInvitationService.js.map +7 -0
  152. package/dist/modules/customer_accounts/services/customerRbacService.js +109 -0
  153. package/dist/modules/customer_accounts/services/customerRbacService.js.map +7 -0
  154. package/dist/modules/customer_accounts/services/customerSessionService.js +75 -0
  155. package/dist/modules/customer_accounts/services/customerSessionService.js.map +7 -0
  156. package/dist/modules/customer_accounts/services/customerTokenService.js +91 -0
  157. package/dist/modules/customer_accounts/services/customerTokenService.js.map +7 -0
  158. package/dist/modules/customer_accounts/services/customerUserService.js +92 -0
  159. package/dist/modules/customer_accounts/services/customerUserService.js.map +7 -0
  160. package/dist/modules/customer_accounts/setup.js +179 -0
  161. package/dist/modules/customer_accounts/setup.js.map +7 -0
  162. package/dist/modules/customer_accounts/subscribers/autoLinkCrm.js +54 -0
  163. package/dist/modules/customer_accounts/subscribers/autoLinkCrm.js.map +7 -0
  164. package/dist/modules/customer_accounts/subscribers/autoLinkCrmReverse.js +68 -0
  165. package/dist/modules/customer_accounts/subscribers/autoLinkCrmReverse.js.map +7 -0
  166. package/dist/modules/customer_accounts/subscribers/notifyStaffOnSignup.js +29 -0
  167. package/dist/modules/customer_accounts/subscribers/notifyStaffOnSignup.js.map +7 -0
  168. package/dist/modules/customer_accounts/translations.js +9 -0
  169. package/dist/modules/customer_accounts/translations.js.map +7 -0
  170. package/dist/modules/customer_accounts/widgets/injection/account-status/widget.client.js +63 -0
  171. package/dist/modules/customer_accounts/widgets/injection/account-status/widget.client.js.map +7 -0
  172. package/dist/modules/customer_accounts/widgets/injection/account-status/widget.js +17 -0
  173. package/dist/modules/customer_accounts/widgets/injection/account-status/widget.js.map +7 -0
  174. package/dist/modules/customer_accounts/widgets/injection/company-users/widget.client.js +55 -0
  175. package/dist/modules/customer_accounts/widgets/injection/company-users/widget.client.js.map +7 -0
  176. package/dist/modules/customer_accounts/widgets/injection/company-users/widget.js +17 -0
  177. package/dist/modules/customer_accounts/widgets/injection/company-users/widget.js.map +7 -0
  178. package/dist/modules/customer_accounts/widgets/injection-table.js +26 -0
  179. package/dist/modules/customer_accounts/widgets/injection-table.js.map +7 -0
  180. package/dist/modules/customer_accounts/workers/cleanupExpiredSessions.js +23 -0
  181. package/dist/modules/customer_accounts/workers/cleanupExpiredSessions.js.map +7 -0
  182. package/dist/modules/customer_accounts/workers/cleanupExpiredTokens.js +38 -0
  183. package/dist/modules/customer_accounts/workers/cleanupExpiredTokens.js.map +7 -0
  184. package/dist/modules/directory/api/get/organizations/lookup.js +83 -0
  185. package/dist/modules/directory/api/get/organizations/lookup.js.map +7 -0
  186. package/dist/modules/directory/commands/organizations.js +32 -1
  187. package/dist/modules/directory/commands/organizations.js.map +2 -2
  188. package/dist/modules/directory/data/entities.js +6 -2
  189. package/dist/modules/directory/data/entities.js.map +2 -2
  190. package/dist/modules/directory/data/validators.js +3 -0
  191. package/dist/modules/directory/data/validators.js.map +2 -2
  192. package/dist/modules/directory/migrations/Migration20260314143323.js +15 -0
  193. package/dist/modules/directory/migrations/Migration20260314143323.js.map +7 -0
  194. package/dist/modules/directory/setup.js +36 -0
  195. package/dist/modules/directory/setup.js.map +2 -2
  196. package/dist/modules/payment_gateways/migrations/Migration20260313222043.js +15 -0
  197. package/dist/modules/payment_gateways/migrations/Migration20260313222043.js.map +7 -0
  198. package/dist/modules/portal/frontend/[orgSlug]/portal/dashboard/page.js +131 -0
  199. package/dist/modules/portal/frontend/[orgSlug]/portal/dashboard/page.js.map +7 -0
  200. package/dist/modules/portal/frontend/[orgSlug]/portal/login/page.js +96 -0
  201. package/dist/modules/portal/frontend/[orgSlug]/portal/login/page.js.map +7 -0
  202. package/dist/modules/portal/frontend/[orgSlug]/portal/page.js +94 -0
  203. package/dist/modules/portal/frontend/[orgSlug]/portal/page.js.map +7 -0
  204. package/dist/modules/portal/frontend/[orgSlug]/portal/profile/page.js +89 -0
  205. package/dist/modules/portal/frontend/[orgSlug]/portal/profile/page.js.map +7 -0
  206. package/dist/modules/portal/frontend/[orgSlug]/portal/signup/page.js +104 -0
  207. package/dist/modules/portal/frontend/[orgSlug]/portal/signup/page.js.map +7 -0
  208. package/dist/modules/portal/index.js +11 -0
  209. package/dist/modules/portal/index.js.map +7 -0
  210. package/dist/modules/portal/setup.js +23 -0
  211. package/dist/modules/portal/setup.js.map +7 -0
  212. package/generated/entities/customer_role/index.ts +12 -0
  213. package/generated/entities/customer_role_acl/index.ts +8 -0
  214. package/generated/entities/customer_user/index.ts +17 -0
  215. package/generated/entities/customer_user_acl/index.ts +8 -0
  216. package/generated/entities/customer_user_email_verification/index.ts +7 -0
  217. package/generated/entities/customer_user_invitation/index.ts +15 -0
  218. package/generated/entities/customer_user_password_reset/index.ts +6 -0
  219. package/generated/entities/customer_user_role/index.ts +5 -0
  220. package/generated/entities/customer_user_session/index.ts +9 -0
  221. package/generated/entities/organization/index.ts +1 -0
  222. package/generated/entities.ids.generated.ts +14 -1
  223. package/generated/entity-fields-registry.ts +18 -0
  224. package/package.json +3 -3
  225. package/src/modules/auth/services/rbacService.ts +3 -9
  226. package/src/modules/customer_accounts/AGENTS.md +377 -0
  227. package/src/modules/customer_accounts/acl.ts +8 -0
  228. package/src/modules/customer_accounts/api/admin/roles/[id]/acl.ts +98 -0
  229. package/src/modules/customer_accounts/api/admin/roles/[id].ts +246 -0
  230. package/src/modules/customer_accounts/api/admin/roles.ts +212 -0
  231. package/src/modules/customer_accounts/api/admin/users/[id]/reset-password.ts +78 -0
  232. package/src/modules/customer_accounts/api/admin/users/[id]/verify-email.ts +72 -0
  233. package/src/modules/customer_accounts/api/admin/users/[id].ts +289 -0
  234. package/src/modules/customer_accounts/api/admin/users-invite.ts +86 -0
  235. package/src/modules/customer_accounts/api/admin/users.ts +280 -0
  236. package/src/modules/customer_accounts/api/email/verify.ts +66 -0
  237. package/src/modules/customer_accounts/api/interceptors.ts +3 -0
  238. package/src/modules/customer_accounts/api/invitations/accept.ts +128 -0
  239. package/src/modules/customer_accounts/api/login.ts +163 -0
  240. package/src/modules/customer_accounts/api/magic-link/request.ts +87 -0
  241. package/src/modules/customer_accounts/api/magic-link/verify.ts +132 -0
  242. package/src/modules/customer_accounts/api/password/reset-confirm.ts +69 -0
  243. package/src/modules/customer_accounts/api/password/reset-request.ts +87 -0
  244. package/src/modules/customer_accounts/api/portal/events/stream.ts +209 -0
  245. package/src/modules/customer_accounts/api/portal/feature-check.ts +60 -0
  246. package/src/modules/customer_accounts/api/portal/logout.ts +71 -0
  247. package/src/modules/customer_accounts/api/portal/notifications/[id]/dismiss.ts +54 -0
  248. package/src/modules/customer_accounts/api/portal/notifications/[id]/read.ts +54 -0
  249. package/src/modules/customer_accounts/api/portal/notifications/mark-all-read.ts +49 -0
  250. package/src/modules/customer_accounts/api/portal/notifications/unread-count.ts +45 -0
  251. package/src/modules/customer_accounts/api/portal/notifications.ts +115 -0
  252. package/src/modules/customer_accounts/api/portal/password-change.ts +65 -0
  253. package/src/modules/customer_accounts/api/portal/profile.ts +151 -0
  254. package/src/modules/customer_accounts/api/portal/sessions/[id].ts +70 -0
  255. package/src/modules/customer_accounts/api/portal/sessions-refresh.ts +87 -0
  256. package/src/modules/customer_accounts/api/portal/sessions.ts +84 -0
  257. package/src/modules/customer_accounts/api/portal/users/[id]/roles.ts +106 -0
  258. package/src/modules/customer_accounts/api/portal/users/[id].ts +81 -0
  259. package/src/modules/customer_accounts/api/portal/users-invite.ts +103 -0
  260. package/src/modules/customer_accounts/api/portal/users.ts +86 -0
  261. package/src/modules/customer_accounts/api/signup.ts +136 -0
  262. package/src/modules/customer_accounts/backend/customer_accounts/[id]/page.meta.ts +11 -0
  263. package/src/modules/customer_accounts/backend/customer_accounts/[id]/page.tsx +607 -0
  264. package/src/modules/customer_accounts/backend/customer_accounts/roles/[id]/page.meta.ts +12 -0
  265. package/src/modules/customer_accounts/backend/customer_accounts/roles/[id]/page.tsx +385 -0
  266. package/src/modules/customer_accounts/backend/customer_accounts/roles/create/page.meta.ts +12 -0
  267. package/src/modules/customer_accounts/backend/customer_accounts/roles/create/page.tsx +203 -0
  268. package/src/modules/customer_accounts/backend/customer_accounts/roles/page.meta.ts +31 -0
  269. package/src/modules/customer_accounts/backend/customer_accounts/roles/page.tsx +217 -0
  270. package/src/modules/customer_accounts/backend/page.meta.ts +33 -0
  271. package/src/modules/customer_accounts/backend/page.tsx +535 -0
  272. package/src/modules/customer_accounts/ce.ts +22 -0
  273. package/src/modules/customer_accounts/data/enrichers.ts +117 -0
  274. package/src/modules/customer_accounts/data/entities.ts +302 -0
  275. package/src/modules/customer_accounts/data/extensions.ts +4 -0
  276. package/src/modules/customer_accounts/data/validators.ts +128 -0
  277. package/src/modules/customer_accounts/di.ts +15 -0
  278. package/src/modules/customer_accounts/events.ts +28 -0
  279. package/src/modules/customer_accounts/i18n/de.json +176 -0
  280. package/src/modules/customer_accounts/i18n/en.json +176 -0
  281. package/src/modules/customer_accounts/i18n/es.json +176 -0
  282. package/src/modules/customer_accounts/i18n/pl.json +176 -0
  283. package/src/modules/customer_accounts/index.ts +13 -0
  284. package/src/modules/customer_accounts/lib/customerAuth.ts +85 -0
  285. package/src/modules/customer_accounts/lib/customerAuthServer.ts +54 -0
  286. package/src/modules/customer_accounts/lib/rateLimiter.ts +36 -0
  287. package/src/modules/customer_accounts/lib/tokenGenerator.ts +9 -0
  288. package/src/modules/customer_accounts/migrations/.snapshot-open-mercato.json +1255 -0
  289. package/src/modules/customer_accounts/migrations/Migration20260313222043.ts +62 -0
  290. package/src/modules/customer_accounts/notifications.client.ts +46 -0
  291. package/src/modules/customer_accounts/notifications.ts +44 -0
  292. package/src/modules/customer_accounts/search.ts +134 -0
  293. package/src/modules/customer_accounts/services/customerInvitationService.ts +109 -0
  294. package/src/modules/customer_accounts/services/customerRbacService.ts +144 -0
  295. package/src/modules/customer_accounts/services/customerSessionService.ts +90 -0
  296. package/src/modules/customer_accounts/services/customerTokenService.ts +98 -0
  297. package/src/modules/customer_accounts/services/customerUserService.ts +105 -0
  298. package/src/modules/customer_accounts/setup.ts +212 -0
  299. package/src/modules/customer_accounts/subscribers/autoLinkCrm.ts +65 -0
  300. package/src/modules/customer_accounts/subscribers/autoLinkCrmReverse.ts +78 -0
  301. package/src/modules/customer_accounts/subscribers/notifyStaffOnSignup.ts +32 -0
  302. package/src/modules/customer_accounts/translations.ts +5 -0
  303. package/src/modules/customer_accounts/widgets/injection/account-status/widget.client.tsx +89 -0
  304. package/src/modules/customer_accounts/widgets/injection/account-status/widget.ts +16 -0
  305. package/src/modules/customer_accounts/widgets/injection/company-users/widget.client.tsx +78 -0
  306. package/src/modules/customer_accounts/widgets/injection/company-users/widget.ts +16 -0
  307. package/src/modules/customer_accounts/widgets/injection-table.ts +24 -0
  308. package/src/modules/customer_accounts/workers/cleanupExpiredSessions.ts +33 -0
  309. package/src/modules/customer_accounts/workers/cleanupExpiredTokens.ts +51 -0
  310. package/src/modules/directory/api/get/organizations/lookup.ts +92 -0
  311. package/src/modules/directory/commands/organizations.ts +34 -1
  312. package/src/modules/directory/data/entities.ts +5 -1
  313. package/src/modules/directory/data/validators.ts +4 -0
  314. package/src/modules/directory/migrations/.snapshot-open-mercato.json +20 -1
  315. package/src/modules/directory/migrations/Migration20260314143323.ts +15 -0
  316. package/src/modules/directory/setup.ts +41 -0
  317. package/src/modules/payment_gateways/migrations/.snapshot-open-mercato.json +4 -1
  318. package/src/modules/payment_gateways/migrations/Migration20260313222043.ts +17 -0
  319. package/src/modules/portal/frontend/[orgSlug]/portal/dashboard/page.tsx +158 -0
  320. package/src/modules/portal/frontend/[orgSlug]/portal/login/page.tsx +120 -0
  321. package/src/modules/portal/frontend/[orgSlug]/portal/page.tsx +118 -0
  322. package/src/modules/portal/frontend/[orgSlug]/portal/profile/page.tsx +112 -0
  323. package/src/modules/portal/frontend/[orgSlug]/portal/signup/page.tsx +138 -0
  324. package/src/modules/portal/i18n/de.json +93 -0
  325. package/src/modules/portal/i18n/en.json +93 -0
  326. package/src/modules/portal/i18n/es.json +93 -0
  327. package/src/modules/portal/i18n/pl.json +93 -0
  328. package/src/modules/portal/index.ts +9 -0
  329. package/src/modules/portal/setup.ts +23 -0
  330. package/src/modules/shipping_carriers/migrations/.snapshot-open-mercato.json +226 -0
@@ -0,0 +1,62 @@
1
+ import { Migration } from '@mikro-orm/migrations';
2
+
3
+ export class Migration20260313222043 extends Migration {
4
+
5
+ override async up(): Promise<void> {
6
+ this.addSql(`create table "customer_roles" ("id" uuid not null default gen_random_uuid(), "tenant_id" uuid not null, "organization_id" uuid not null, "name" text not null, "slug" text not null, "description" text null, "is_default" boolean not null default false, "is_system" boolean not null default false, "customer_assignable" boolean not null default false, "created_at" timestamptz not null, "updated_at" timestamptz null, "deleted_at" timestamptz null, constraint "customer_roles_pkey" primary key ("id"));`);
7
+ this.addSql(`alter table "customer_roles" add constraint "customer_roles_tenant_slug_uniq" unique ("tenant_id", "slug");`);
8
+
9
+ this.addSql(`create table "customer_role_acls" ("id" uuid not null default gen_random_uuid(), "role_id" uuid not null, "tenant_id" uuid not null, "features_json" jsonb null, "is_portal_admin" boolean not null default false, "created_at" timestamptz not null, "updated_at" timestamptz null, "deleted_at" timestamptz null, constraint "customer_role_acls_pkey" primary key ("id"));`);
10
+ this.addSql(`alter table "customer_role_acls" add constraint "customer_role_acls_role_tenant_uniq" unique ("role_id", "tenant_id");`);
11
+
12
+ this.addSql(`create table "customer_users" ("id" uuid not null default gen_random_uuid(), "tenant_id" uuid not null, "organization_id" uuid not null, "email" text not null, "email_hash" text not null, "password_hash" text null, "display_name" text not null, "email_verified_at" timestamptz null, "failed_login_attempts" int not null default 0, "locked_until" timestamptz null, "last_login_at" timestamptz null, "person_entity_id" uuid null, "customer_entity_id" uuid null, "is_active" boolean not null default true, "created_at" timestamptz not null, "updated_at" timestamptz null, "deleted_at" timestamptz null, constraint "customer_users_pkey" primary key ("id"));`);
13
+ this.addSql(`create index "customer_users_email_hash_idx" on "customer_users" ("email_hash");`);
14
+ this.addSql(`create index "customer_users_person_entity_idx" on "customer_users" ("person_entity_id");`);
15
+ this.addSql(`create index "customer_users_customer_entity_idx" on "customer_users" ("customer_entity_id");`);
16
+ this.addSql(`alter table "customer_users" add constraint "customer_users_tenant_email_hash_uniq" unique ("tenant_id", "email_hash");`);
17
+
18
+ this.addSql(`create table "customer_user_acls" ("id" uuid not null default gen_random_uuid(), "user_id" uuid not null, "tenant_id" uuid not null, "features_json" jsonb null, "is_portal_admin" boolean not null default false, "created_at" timestamptz not null, "updated_at" timestamptz null, "deleted_at" timestamptz null, constraint "customer_user_acls_pkey" primary key ("id"));`);
19
+ this.addSql(`alter table "customer_user_acls" add constraint "customer_user_acls_user_tenant_uniq" unique ("user_id", "tenant_id");`);
20
+
21
+ this.addSql(`create table "customer_user_email_verifications" ("id" uuid not null default gen_random_uuid(), "user_id" uuid not null, "token" text not null, "purpose" text not null default 'email_verification', "expires_at" timestamptz not null, "used_at" timestamptz null, "created_at" timestamptz not null, constraint "customer_user_email_verifications_pkey" primary key ("id"));`);
22
+ this.addSql(`create index "customer_user_email_verifications_token_idx" on "customer_user_email_verifications" ("token");`);
23
+
24
+ this.addSql(`create table "customer_user_invitations" ("id" uuid not null default gen_random_uuid(), "tenant_id" uuid not null, "organization_id" uuid not null, "email" text not null, "email_hash" text not null, "token" text not null, "customer_entity_id" uuid null, "role_ids_json" jsonb null, "invited_by_user_id" uuid null, "invited_by_customer_user_id" uuid null, "display_name" text null, "expires_at" timestamptz not null, "accepted_at" timestamptz null, "cancelled_at" timestamptz null, "created_at" timestamptz not null, constraint "customer_user_invitations_pkey" primary key ("id"));`);
25
+ this.addSql(`create index "customer_user_invitations_tenant_email_hash_idx" on "customer_user_invitations" ("tenant_id", "email_hash");`);
26
+ this.addSql(`create index "customer_user_invitations_token_idx" on "customer_user_invitations" ("token");`);
27
+
28
+ this.addSql(`create table "customer_user_password_resets" ("id" uuid not null default gen_random_uuid(), "user_id" uuid not null, "token" text not null, "expires_at" timestamptz not null, "used_at" timestamptz null, "created_at" timestamptz not null, constraint "customer_user_password_resets_pkey" primary key ("id"));`);
29
+ this.addSql(`create index "customer_user_password_resets_token_idx" on "customer_user_password_resets" ("token");`);
30
+
31
+ this.addSql(`create table "customer_user_roles" ("id" uuid not null default gen_random_uuid(), "user_id" uuid not null, "role_id" uuid not null, "created_at" timestamptz not null, "deleted_at" timestamptz null, constraint "customer_user_roles_pkey" primary key ("id"));`);
32
+ this.addSql(`alter table "customer_user_roles" add constraint "customer_user_roles_user_role_uniq" unique ("user_id", "role_id");`);
33
+
34
+ this.addSql(`create table "customer_user_sessions" ("id" uuid not null default gen_random_uuid(), "user_id" uuid not null, "token_hash" text not null, "ip_address" text null, "user_agent" text null, "expires_at" timestamptz not null, "last_used_at" timestamptz null, "created_at" timestamptz not null, "deleted_at" timestamptz null, constraint "customer_user_sessions_pkey" primary key ("id"));`);
35
+ this.addSql(`create index "customer_user_sessions_token_hash_idx" on "customer_user_sessions" ("token_hash");`);
36
+
37
+ this.addSql(`alter table "customer_role_acls" add constraint "customer_role_acls_role_id_foreign" foreign key ("role_id") references "customer_roles" ("id") on update cascade;`);
38
+
39
+ this.addSql(`alter table "customer_user_acls" add constraint "customer_user_acls_user_id_foreign" foreign key ("user_id") references "customer_users" ("id") on update cascade;`);
40
+
41
+ this.addSql(`alter table "customer_user_email_verifications" add constraint "customer_user_email_verifications_user_id_foreign" foreign key ("user_id") references "customer_users" ("id") on update cascade;`);
42
+
43
+ this.addSql(`alter table "customer_user_password_resets" add constraint "customer_user_password_resets_user_id_foreign" foreign key ("user_id") references "customer_users" ("id") on update cascade;`);
44
+
45
+ this.addSql(`alter table "customer_user_roles" add constraint "customer_user_roles_user_id_foreign" foreign key ("user_id") references "customer_users" ("id") on update cascade;`);
46
+ this.addSql(`alter table "customer_user_roles" add constraint "customer_user_roles_role_id_foreign" foreign key ("role_id") references "customer_roles" ("id") on update cascade;`);
47
+
48
+ this.addSql(`alter table "customer_user_sessions" add constraint "customer_user_sessions_user_id_foreign" foreign key ("user_id") references "customer_users" ("id") on update cascade;`);
49
+ }
50
+
51
+ override async down(): Promise<void> {
52
+ this.addSql(`drop table if exists "customer_user_sessions" cascade;`);
53
+ this.addSql(`drop table if exists "customer_user_roles" cascade;`);
54
+ this.addSql(`drop table if exists "customer_user_password_resets" cascade;`);
55
+ this.addSql(`drop table if exists "customer_user_invitations" cascade;`);
56
+ this.addSql(`drop table if exists "customer_user_email_verifications" cascade;`);
57
+ this.addSql(`drop table if exists "customer_user_acls" cascade;`);
58
+ this.addSql(`drop table if exists "customer_role_acls" cascade;`);
59
+ this.addSql(`drop table if exists "customer_users" cascade;`);
60
+ this.addSql(`drop table if exists "customer_roles" cascade;`);
61
+ }
62
+ }
@@ -0,0 +1,46 @@
1
+ 'use client'
2
+
3
+ import type { NotificationTypeDefinition } from '@open-mercato/shared/modules/notifications/types'
4
+
5
+ export const customerAccountsNotificationTypes: NotificationTypeDefinition[] = [
6
+ {
7
+ type: 'customer_accounts.user.signup',
8
+ module: 'customer_accounts',
9
+ titleKey: 'customer_accounts.notifications.user.signup.title',
10
+ bodyKey: 'customer_accounts.notifications.user.signup.body',
11
+ icon: 'user-plus',
12
+ severity: 'info',
13
+ actions: [
14
+ {
15
+ id: 'view',
16
+ labelKey: 'common.view',
17
+ variant: 'outline',
18
+ href: '/backend/customer_accounts/{sourceEntityId}',
19
+ icon: 'external-link',
20
+ },
21
+ ],
22
+ linkHref: '/backend/customer_accounts/{sourceEntityId}',
23
+ expiresAfterHours: 168,
24
+ },
25
+ {
26
+ type: 'customer_accounts.user.locked',
27
+ module: 'customer_accounts',
28
+ titleKey: 'customer_accounts.notifications.user.locked.title',
29
+ bodyKey: 'customer_accounts.notifications.user.locked.body',
30
+ icon: 'lock',
31
+ severity: 'warning',
32
+ actions: [
33
+ {
34
+ id: 'view',
35
+ labelKey: 'common.view',
36
+ variant: 'outline',
37
+ href: '/backend/customer_accounts/{sourceEntityId}',
38
+ icon: 'external-link',
39
+ },
40
+ ],
41
+ linkHref: '/backend/customer_accounts/{sourceEntityId}',
42
+ expiresAfterHours: 168,
43
+ },
44
+ ]
45
+
46
+ export default customerAccountsNotificationTypes
@@ -0,0 +1,44 @@
1
+ import type { NotificationTypeDefinition } from '@open-mercato/shared/modules/notifications/types'
2
+
3
+ export const notificationTypes: NotificationTypeDefinition[] = [
4
+ {
5
+ type: 'customer_accounts.user.signup',
6
+ module: 'customer_accounts',
7
+ titleKey: 'customer_accounts.notifications.user.signup.title',
8
+ bodyKey: 'customer_accounts.notifications.user.signup.body',
9
+ icon: 'user-plus',
10
+ severity: 'info',
11
+ actions: [
12
+ {
13
+ id: 'view',
14
+ labelKey: 'common.view',
15
+ variant: 'outline',
16
+ href: '/backend/customer_accounts/{sourceEntityId}',
17
+ icon: 'external-link',
18
+ },
19
+ ],
20
+ linkHref: '/backend/customer_accounts/{sourceEntityId}',
21
+ expiresAfterHours: 168,
22
+ },
23
+ {
24
+ type: 'customer_accounts.user.locked',
25
+ module: 'customer_accounts',
26
+ titleKey: 'customer_accounts.notifications.user.locked.title',
27
+ bodyKey: 'customer_accounts.notifications.user.locked.body',
28
+ icon: 'lock',
29
+ severity: 'warning',
30
+ actions: [
31
+ {
32
+ id: 'view',
33
+ labelKey: 'common.view',
34
+ variant: 'outline',
35
+ href: '/backend/customer_accounts/{sourceEntityId}',
36
+ icon: 'external-link',
37
+ },
38
+ ],
39
+ linkHref: '/backend/customer_accounts/{sourceEntityId}',
40
+ expiresAfterHours: 168,
41
+ },
42
+ ]
43
+
44
+ export default notificationTypes
@@ -0,0 +1,134 @@
1
+ import type {
2
+ SearchModuleConfig,
3
+ SearchBuildContext,
4
+ SearchResultPresenter,
5
+ SearchIndexSource,
6
+ } from '@open-mercato/shared/modules/search'
7
+ import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
8
+
9
+ function pickString(...candidates: Array<unknown>): string | null {
10
+ for (const candidate of candidates) {
11
+ if (typeof candidate !== 'string') continue
12
+ const trimmed = candidate.trim()
13
+ if (trimmed.length > 0) return trimmed
14
+ }
15
+ return null
16
+ }
17
+
18
+ function snippet(value: unknown, max = 140): string | undefined {
19
+ if (typeof value !== 'string') return undefined
20
+ const trimmed = value.trim()
21
+ if (!trimmed.length) return undefined
22
+ if (trimmed.length <= max) return trimmed
23
+ return `${trimmed.slice(0, max - 3)}...`
24
+ }
25
+
26
+ function appendLine(lines: string[], label: string, value: unknown) {
27
+ if (value === null || value === undefined) return
28
+ const text = typeof value === 'object' ? JSON.stringify(value) : String(value)
29
+ if (!text.trim()) return
30
+ lines.push(`${label}: ${text}`)
31
+ }
32
+
33
+ function formatSubtitle(...parts: Array<unknown>): string | undefined {
34
+ const text = parts
35
+ .map((part) => (part === null || part === undefined ? '' : String(part)))
36
+ .map((part) => part.trim())
37
+ .filter(Boolean)
38
+ if (text.length === 0) return undefined
39
+ return text.join(' · ')
40
+ }
41
+
42
+ function buildIndexSource(
43
+ ctx: SearchBuildContext,
44
+ presenter: SearchResultPresenter,
45
+ lines: string[],
46
+ ): SearchIndexSource | null {
47
+ if (!lines.length) return null
48
+ return {
49
+ text: lines,
50
+ presenter,
51
+ checksumSource: { record: ctx.record, customFields: ctx.customFields },
52
+ }
53
+ }
54
+
55
+ export const searchConfig: SearchModuleConfig = {
56
+ entities: [
57
+ {
58
+ entityId: 'customer_accounts:customer_user',
59
+ enabled: true,
60
+ priority: 6,
61
+ buildSource: async (ctx) => {
62
+ const { t } = await resolveTranslations()
63
+ const record = ctx.record
64
+ const lines: string[] = []
65
+ appendLine(lines, 'Name', record.display_name ?? record.displayName)
66
+ appendLine(lines, 'Email', record.email)
67
+ return buildIndexSource(
68
+ ctx,
69
+ {
70
+ title: pickString(record.display_name, record.displayName) ?? String(record.id),
71
+ subtitle: formatSubtitle(record.email),
72
+ icon: 'user',
73
+ badge: t('customer_accounts.search.badge.customerUser', 'Customer User'),
74
+ },
75
+ lines,
76
+ )
77
+ },
78
+ formatResult: async (ctx) => {
79
+ const { t } = await resolveTranslations()
80
+ const record = ctx.record
81
+ return {
82
+ title: pickString(record.display_name, record.displayName) ?? String(record.id),
83
+ subtitle: formatSubtitle(record.email),
84
+ icon: 'user',
85
+ badge: t('customer_accounts.search.badge.customerUser', 'Customer User'),
86
+ }
87
+ },
88
+ resolveUrl: async (ctx) => `/backend/customer-accounts/users/${encodeURIComponent(String(ctx.record.id))}`,
89
+ fieldPolicy: {
90
+ searchable: ['display_name', 'email'],
91
+ excluded: ['password_hash', 'email_hash'],
92
+ },
93
+ },
94
+ {
95
+ entityId: 'customer_accounts:customer_role',
96
+ enabled: true,
97
+ priority: 6,
98
+ buildSource: async (ctx) => {
99
+ const { t } = await resolveTranslations()
100
+ const record = ctx.record
101
+ const lines: string[] = []
102
+ appendLine(lines, 'Name', record.name)
103
+ appendLine(lines, 'Description', record.description)
104
+ return buildIndexSource(
105
+ ctx,
106
+ {
107
+ title: pickString(record.name) ?? String(record.id),
108
+ subtitle: formatSubtitle(snippet(record.description)),
109
+ icon: 'shield',
110
+ badge: t('customer_accounts.search.badge.customerRole', 'Customer Role'),
111
+ },
112
+ lines,
113
+ )
114
+ },
115
+ formatResult: async (ctx) => {
116
+ const { t } = await resolveTranslations()
117
+ const record = ctx.record
118
+ return {
119
+ title: pickString(record.name) ?? String(record.id),
120
+ subtitle: formatSubtitle(snippet(record.description)),
121
+ icon: 'shield',
122
+ badge: t('customer_accounts.search.badge.customerRole', 'Customer Role'),
123
+ }
124
+ },
125
+ resolveUrl: async (ctx) => `/backend/customer-accounts/roles/${encodeURIComponent(String(ctx.record.id))}/edit`,
126
+ fieldPolicy: {
127
+ searchable: ['name', 'description'],
128
+ },
129
+ },
130
+ ],
131
+ }
132
+
133
+ export default searchConfig
134
+ export const config = searchConfig
@@ -0,0 +1,109 @@
1
+ import { EntityManager } from '@mikro-orm/postgresql'
2
+ import { hash } from 'bcryptjs'
3
+ import {
4
+ CustomerUser,
5
+ CustomerUserInvitation,
6
+ CustomerUserRole,
7
+ CustomerRole,
8
+ } from '@open-mercato/core/modules/customer_accounts/data/entities'
9
+ import { generateSecureToken, hashToken } from '@open-mercato/core/modules/customer_accounts/lib/tokenGenerator'
10
+ import { hashForLookup } from '@open-mercato/shared/lib/encryption/aes'
11
+
12
+ const BCRYPT_COST = 10
13
+ const INVITATION_TTL_MS = 72 * 60 * 60 * 1000 // 72 hours
14
+
15
+ export class CustomerInvitationService {
16
+ constructor(private em: EntityManager) {}
17
+
18
+ async createInvitation(
19
+ email: string,
20
+ scope: { tenantId: string; organizationId: string },
21
+ options: {
22
+ customerEntityId?: string | null
23
+ roleIds: string[]
24
+ invitedByUserId?: string | null
25
+ invitedByCustomerUserId?: string | null
26
+ displayName?: string | null
27
+ },
28
+ ): Promise<{ invitation: CustomerUserInvitation; rawToken: string }> {
29
+ const token = generateSecureToken()
30
+ const emailHash = hashForLookup(email)
31
+ const expiresAt = new Date(Date.now() + INVITATION_TTL_MS)
32
+
33
+ const tokenHashed = hashToken(token)
34
+ const invitation = this.em.create(CustomerUserInvitation, {
35
+ tenantId: scope.tenantId,
36
+ organizationId: scope.organizationId,
37
+ email: email.toLowerCase().trim(),
38
+ emailHash,
39
+ token: tokenHashed,
40
+ customerEntityId: options.customerEntityId || null,
41
+ roleIdsJson: options.roleIds,
42
+ invitedByUserId: options.invitedByUserId || null,
43
+ invitedByCustomerUserId: options.invitedByCustomerUserId || null,
44
+ displayName: options.displayName || null,
45
+ expiresAt,
46
+ createdAt: new Date(),
47
+ } as any) as CustomerUserInvitation
48
+ await this.em.persistAndFlush(invitation)
49
+ return { invitation, rawToken: token }
50
+ }
51
+
52
+ async findByToken(token: string): Promise<CustomerUserInvitation | null> {
53
+ const tokenHashed = hashToken(token)
54
+ const invitation = await this.em.findOne(CustomerUserInvitation, { token: tokenHashed })
55
+ if (!invitation) return null
56
+ if (invitation.acceptedAt) return null
57
+ if (invitation.cancelledAt) return null
58
+ if (invitation.expiresAt.getTime() < Date.now()) return null
59
+ return invitation
60
+ }
61
+
62
+ async acceptInvitation(
63
+ token: string,
64
+ password: string,
65
+ displayName: string,
66
+ ): Promise<{ user: CustomerUser; invitation: CustomerUserInvitation } | null> {
67
+ const invitation = await this.findByToken(token)
68
+ if (!invitation) return null
69
+
70
+ const passwordHash = await hash(password, BCRYPT_COST)
71
+ const emailHash = hashForLookup(invitation.email)
72
+
73
+ // Create user
74
+ const user = this.em.create(CustomerUser, {
75
+ email: invitation.email,
76
+ emailHash,
77
+ passwordHash,
78
+ displayName: displayName || invitation.displayName || invitation.email,
79
+ tenantId: invitation.tenantId,
80
+ organizationId: invitation.organizationId,
81
+ customerEntityId: invitation.customerEntityId || null,
82
+ isActive: true,
83
+ emailVerifiedAt: new Date(), // Invitation implicitly verifies email
84
+ failedLoginAttempts: 0,
85
+ createdAt: new Date(),
86
+ } as any) as CustomerUser
87
+ this.em.persist(user)
88
+
89
+ // Assign roles
90
+ const roleIds = Array.isArray(invitation.roleIdsJson) ? invitation.roleIdsJson : []
91
+ for (const roleId of roleIds) {
92
+ const role = await this.em.findOne(CustomerRole, { id: roleId, tenantId: invitation.tenantId, deletedAt: null })
93
+ if (role) {
94
+ const userRole = this.em.create(CustomerUserRole, {
95
+ user,
96
+ role,
97
+ createdAt: new Date(),
98
+ } as any)
99
+ this.em.persist(userRole)
100
+ }
101
+ }
102
+
103
+ // Mark invitation as accepted
104
+ invitation.acceptedAt = new Date()
105
+
106
+ await this.em.flush()
107
+ return { user, invitation }
108
+ }
109
+ }
@@ -0,0 +1,144 @@
1
+ import { EntityManager } from '@mikro-orm/postgresql'
2
+ import type { CacheStrategy } from '@open-mercato/cache'
3
+ import {
4
+ CustomerUserAcl,
5
+ CustomerRoleAcl,
6
+ CustomerUserRole,
7
+ } from '@open-mercato/core/modules/customer_accounts/data/entities'
8
+ import { hasAllFeatures } from '@open-mercato/shared/lib/auth/featureMatch'
9
+
10
+ interface CustomerAclData {
11
+ isPortalAdmin: boolean
12
+ features: string[]
13
+ }
14
+
15
+ function isCustomerAclData(value: unknown): value is CustomerAclData {
16
+ if (typeof value !== 'object' || value === null) return false
17
+ const record = value as Partial<CustomerAclData>
18
+ if (typeof record.isPortalAdmin !== 'boolean') return false
19
+ if (!Array.isArray(record.features) || record.features.some((f) => typeof f !== 'string')) return false
20
+ return true
21
+ }
22
+
23
+ export class CustomerRbacService {
24
+ private cacheTtlMs: number = 5 * 60 * 1000
25
+ private cache: CacheStrategy | null = null
26
+
27
+ constructor(private em: EntityManager, cache?: CacheStrategy) {
28
+ this.cache = cache || null
29
+ }
30
+
31
+ private getCacheKey(userId: string, scope: { tenantId: string; organizationId: string }): string {
32
+ return `customer_rbac:${userId}:${scope.tenantId}:${scope.organizationId}`
33
+ }
34
+
35
+ private getUserTag(userId: string): string {
36
+ return `customer_rbac:user:${userId}`
37
+ }
38
+
39
+ private getTenantTag(tenantId: string): string {
40
+ return `customer_rbac:tenant:${tenantId}`
41
+ }
42
+
43
+ private async getFromCache(cacheKey: string): Promise<CustomerAclData | null> {
44
+ if (!this.cache) return null
45
+ const cached = await this.cache.get(cacheKey)
46
+ if (!cached) return null
47
+ return isCustomerAclData(cached) ? cached : null
48
+ }
49
+
50
+ private async setCache(
51
+ cacheKey: string,
52
+ data: CustomerAclData,
53
+ userId: string,
54
+ scope: { tenantId: string; organizationId: string },
55
+ ): Promise<void> {
56
+ if (!this.cache) return
57
+ const tags = [
58
+ this.getUserTag(userId),
59
+ this.getTenantTag(scope.tenantId),
60
+ 'customer_rbac:all',
61
+ ]
62
+ await this.cache.set(cacheKey, data, { ttl: this.cacheTtlMs, tags })
63
+ }
64
+
65
+ async loadAcl(
66
+ userId: string,
67
+ scope: { tenantId: string; organizationId: string },
68
+ ): Promise<CustomerAclData> {
69
+ const cacheKey = this.getCacheKey(userId, scope)
70
+ const cached = await this.getFromCache(cacheKey)
71
+ if (cached) return cached
72
+
73
+ const em = this.em.fork()
74
+
75
+ // Per-user ACL first
76
+ const userAcl = await em.findOne(CustomerUserAcl, {
77
+ user: userId as any,
78
+ tenantId: scope.tenantId,
79
+ })
80
+ if (userAcl) {
81
+ const result: CustomerAclData = {
82
+ isPortalAdmin: !!userAcl.isPortalAdmin,
83
+ features: Array.isArray(userAcl.featuresJson) ? userAcl.featuresJson : [],
84
+ }
85
+ await this.setCache(cacheKey, result, userId, scope)
86
+ return result
87
+ }
88
+
89
+ // Aggregate role ACLs
90
+ const links = await em.find(CustomerUserRole, {
91
+ user: userId as any,
92
+ deletedAt: null,
93
+ }, { populate: ['role'] })
94
+ const roleIds = links.map((l) => (l.role as any)?.id).filter(Boolean)
95
+
96
+ let isPortalAdmin = false
97
+ const features: string[] = []
98
+ if (roleIds.length) {
99
+ const roleAcls = await em.find(CustomerRoleAcl, {
100
+ tenantId: scope.tenantId,
101
+ role: { $in: roleIds as any },
102
+ } as any)
103
+ for (const acl of roleAcls) {
104
+ isPortalAdmin = isPortalAdmin || !!acl.isPortalAdmin
105
+ if (Array.isArray(acl.featuresJson)) {
106
+ for (const f of acl.featuresJson) {
107
+ if (!features.includes(f)) features.push(f)
108
+ }
109
+ }
110
+ }
111
+ }
112
+
113
+ const result: CustomerAclData = { isPortalAdmin, features }
114
+ await this.setCache(cacheKey, result, userId, scope)
115
+ return result
116
+ }
117
+
118
+ async userHasAllFeatures(
119
+ userId: string,
120
+ required: string[],
121
+ scope: { tenantId: string; organizationId: string },
122
+ ): Promise<boolean> {
123
+ if (!required.length) return true
124
+ const acl = await this.loadAcl(userId, scope)
125
+ if (acl.isPortalAdmin) return true
126
+ return hasAllFeatures(required, acl.features)
127
+ }
128
+
129
+ async invalidateUserCache(userId: string): Promise<void> {
130
+ if (!this.cache) return
131
+ await this.cache.deleteByTags([this.getUserTag(userId)])
132
+ }
133
+
134
+ async invalidateRoleCache(roleId: string): Promise<void> {
135
+ if (!this.cache) return
136
+ // When a role changes, invalidate all customer RBAC caches since we don't track role→user mappings
137
+ await this.cache.deleteByTags(['customer_rbac:all'])
138
+ }
139
+
140
+ async invalidateTenantCache(tenantId: string): Promise<void> {
141
+ if (!this.cache) return
142
+ await this.cache.deleteByTags([this.getTenantTag(tenantId)])
143
+ }
144
+ }
@@ -0,0 +1,90 @@
1
+ import { EntityManager } from '@mikro-orm/postgresql'
2
+ import { CustomerUser, CustomerUserSession } from '@open-mercato/core/modules/customer_accounts/data/entities'
3
+ import { generateSecureToken, hashToken } from '@open-mercato/core/modules/customer_accounts/lib/tokenGenerator'
4
+ import { signJwt } from '@open-mercato/shared/lib/auth/jwt'
5
+
6
+ const DEFAULT_SESSION_TTL_DAYS = 30
7
+
8
+ export class CustomerSessionService {
9
+ constructor(private em: EntityManager) {}
10
+
11
+ async createSession(
12
+ user: CustomerUser,
13
+ resolvedFeatures: string[],
14
+ ip?: string | null,
15
+ userAgent?: string | null,
16
+ ): Promise<{ rawToken: string; jwt: string; session: CustomerUserSession }> {
17
+ const rawToken = generateSecureToken()
18
+ const tokenHash = hashToken(rawToken)
19
+ const days = Number(process.env.CUSTOMER_SESSION_TTL_DAYS || DEFAULT_SESSION_TTL_DAYS)
20
+ const expiresAt = new Date(Date.now() + days * 24 * 60 * 60 * 1000)
21
+
22
+ const session = this.em.create(CustomerUserSession, {
23
+ user,
24
+ tokenHash,
25
+ ipAddress: ip || null,
26
+ userAgent: userAgent || null,
27
+ expiresAt,
28
+ lastUsedAt: new Date(),
29
+ createdAt: new Date(),
30
+ } as any) as CustomerUserSession
31
+ await this.em.persistAndFlush(session)
32
+
33
+ const jwt = this.signCustomerJwt(user, resolvedFeatures)
34
+
35
+ return { rawToken, jwt, session }
36
+ }
37
+
38
+ signCustomerJwt(user: CustomerUser, resolvedFeatures: string[]): string {
39
+ return signJwt({
40
+ sub: user.id,
41
+ type: 'customer',
42
+ tenantId: user.tenantId,
43
+ orgId: user.organizationId,
44
+ email: user.email,
45
+ displayName: user.displayName || '',
46
+ customerEntityId: user.customerEntityId || null,
47
+ personEntityId: user.personEntityId || null,
48
+ resolvedFeatures,
49
+ })
50
+ }
51
+
52
+ async findByToken(rawToken: string, tenantId?: string): Promise<CustomerUserSession | null> {
53
+ const tokenHash = hashToken(rawToken)
54
+ const session = await this.em.findOne(CustomerUserSession, {
55
+ tokenHash,
56
+ deletedAt: null,
57
+ }, { populate: ['user'] })
58
+ if (!session) return null
59
+ if (session.expiresAt.getTime() < Date.now()) return null
60
+ const user = session.user as CustomerUser
61
+ if (tenantId && user?.tenantId !== tenantId) return null
62
+ return session
63
+ }
64
+
65
+ async refreshSession(
66
+ rawToken: string,
67
+ resolvedFeatures: string[],
68
+ ): Promise<{ jwt: string; user: CustomerUser } | null> {
69
+ const session = await this.findByToken(rawToken)
70
+ if (!session) return null
71
+ const user = session.user as CustomerUser
72
+ if (!user || user.deletedAt || !user.isActive) return null
73
+
74
+ await this.em.nativeUpdate(CustomerUserSession, { id: session.id }, { lastUsedAt: new Date() })
75
+ const jwt = this.signCustomerJwt(user, resolvedFeatures)
76
+ return { jwt, user }
77
+ }
78
+
79
+ async revokeSession(sessionId: string): Promise<void> {
80
+ await this.em.nativeUpdate(CustomerUserSession, { id: sessionId }, { deletedAt: new Date() })
81
+ }
82
+
83
+ async revokeAllUserSessions(userId: string): Promise<void> {
84
+ await this.em.nativeUpdate(
85
+ CustomerUserSession,
86
+ { user: userId as any, deletedAt: null },
87
+ { deletedAt: new Date() },
88
+ )
89
+ }
90
+ }