@open-mercato/core 0.4.8-develop-28cee031d6 → 0.4.8-develop-15259be22b

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 (333) 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/customers/components/AddressTiles.js +1 -1
  185. package/dist/modules/customers/components/AddressTiles.js.map +2 -2
  186. package/dist/modules/directory/api/get/organizations/lookup.js +83 -0
  187. package/dist/modules/directory/api/get/organizations/lookup.js.map +7 -0
  188. package/dist/modules/directory/commands/organizations.js +32 -1
  189. package/dist/modules/directory/commands/organizations.js.map +2 -2
  190. package/dist/modules/directory/data/entities.js +6 -2
  191. package/dist/modules/directory/data/entities.js.map +2 -2
  192. package/dist/modules/directory/data/validators.js +3 -0
  193. package/dist/modules/directory/data/validators.js.map +2 -2
  194. package/dist/modules/directory/migrations/Migration20260314143323.js +15 -0
  195. package/dist/modules/directory/migrations/Migration20260314143323.js.map +7 -0
  196. package/dist/modules/directory/setup.js +36 -0
  197. package/dist/modules/directory/setup.js.map +2 -2
  198. package/dist/modules/payment_gateways/migrations/Migration20260313222043.js +15 -0
  199. package/dist/modules/payment_gateways/migrations/Migration20260313222043.js.map +7 -0
  200. package/dist/modules/portal/frontend/[orgSlug]/portal/dashboard/page.js +131 -0
  201. package/dist/modules/portal/frontend/[orgSlug]/portal/dashboard/page.js.map +7 -0
  202. package/dist/modules/portal/frontend/[orgSlug]/portal/login/page.js +96 -0
  203. package/dist/modules/portal/frontend/[orgSlug]/portal/login/page.js.map +7 -0
  204. package/dist/modules/portal/frontend/[orgSlug]/portal/page.js +94 -0
  205. package/dist/modules/portal/frontend/[orgSlug]/portal/page.js.map +7 -0
  206. package/dist/modules/portal/frontend/[orgSlug]/portal/profile/page.js +89 -0
  207. package/dist/modules/portal/frontend/[orgSlug]/portal/profile/page.js.map +7 -0
  208. package/dist/modules/portal/frontend/[orgSlug]/portal/signup/page.js +104 -0
  209. package/dist/modules/portal/frontend/[orgSlug]/portal/signup/page.js.map +7 -0
  210. package/dist/modules/portal/index.js +11 -0
  211. package/dist/modules/portal/index.js.map +7 -0
  212. package/dist/modules/portal/setup.js +23 -0
  213. package/dist/modules/portal/setup.js.map +7 -0
  214. package/generated/entities/customer_role/index.ts +12 -0
  215. package/generated/entities/customer_role_acl/index.ts +8 -0
  216. package/generated/entities/customer_user/index.ts +17 -0
  217. package/generated/entities/customer_user_acl/index.ts +8 -0
  218. package/generated/entities/customer_user_email_verification/index.ts +7 -0
  219. package/generated/entities/customer_user_invitation/index.ts +15 -0
  220. package/generated/entities/customer_user_password_reset/index.ts +6 -0
  221. package/generated/entities/customer_user_role/index.ts +5 -0
  222. package/generated/entities/customer_user_session/index.ts +9 -0
  223. package/generated/entities/organization/index.ts +1 -0
  224. package/generated/entities.ids.generated.ts +14 -1
  225. package/generated/entity-fields-registry.ts +18 -0
  226. package/package.json +3 -3
  227. package/src/modules/auth/services/rbacService.ts +3 -9
  228. package/src/modules/customer_accounts/AGENTS.md +377 -0
  229. package/src/modules/customer_accounts/acl.ts +8 -0
  230. package/src/modules/customer_accounts/api/admin/roles/[id]/acl.ts +98 -0
  231. package/src/modules/customer_accounts/api/admin/roles/[id].ts +246 -0
  232. package/src/modules/customer_accounts/api/admin/roles.ts +212 -0
  233. package/src/modules/customer_accounts/api/admin/users/[id]/reset-password.ts +78 -0
  234. package/src/modules/customer_accounts/api/admin/users/[id]/verify-email.ts +72 -0
  235. package/src/modules/customer_accounts/api/admin/users/[id].ts +289 -0
  236. package/src/modules/customer_accounts/api/admin/users-invite.ts +86 -0
  237. package/src/modules/customer_accounts/api/admin/users.ts +280 -0
  238. package/src/modules/customer_accounts/api/email/verify.ts +66 -0
  239. package/src/modules/customer_accounts/api/interceptors.ts +3 -0
  240. package/src/modules/customer_accounts/api/invitations/accept.ts +128 -0
  241. package/src/modules/customer_accounts/api/login.ts +163 -0
  242. package/src/modules/customer_accounts/api/magic-link/request.ts +87 -0
  243. package/src/modules/customer_accounts/api/magic-link/verify.ts +132 -0
  244. package/src/modules/customer_accounts/api/password/reset-confirm.ts +69 -0
  245. package/src/modules/customer_accounts/api/password/reset-request.ts +87 -0
  246. package/src/modules/customer_accounts/api/portal/events/stream.ts +209 -0
  247. package/src/modules/customer_accounts/api/portal/feature-check.ts +60 -0
  248. package/src/modules/customer_accounts/api/portal/logout.ts +71 -0
  249. package/src/modules/customer_accounts/api/portal/notifications/[id]/dismiss.ts +54 -0
  250. package/src/modules/customer_accounts/api/portal/notifications/[id]/read.ts +54 -0
  251. package/src/modules/customer_accounts/api/portal/notifications/mark-all-read.ts +49 -0
  252. package/src/modules/customer_accounts/api/portal/notifications/unread-count.ts +45 -0
  253. package/src/modules/customer_accounts/api/portal/notifications.ts +115 -0
  254. package/src/modules/customer_accounts/api/portal/password-change.ts +65 -0
  255. package/src/modules/customer_accounts/api/portal/profile.ts +151 -0
  256. package/src/modules/customer_accounts/api/portal/sessions/[id].ts +70 -0
  257. package/src/modules/customer_accounts/api/portal/sessions-refresh.ts +87 -0
  258. package/src/modules/customer_accounts/api/portal/sessions.ts +84 -0
  259. package/src/modules/customer_accounts/api/portal/users/[id]/roles.ts +106 -0
  260. package/src/modules/customer_accounts/api/portal/users/[id].ts +81 -0
  261. package/src/modules/customer_accounts/api/portal/users-invite.ts +103 -0
  262. package/src/modules/customer_accounts/api/portal/users.ts +86 -0
  263. package/src/modules/customer_accounts/api/signup.ts +136 -0
  264. package/src/modules/customer_accounts/backend/customer_accounts/[id]/page.meta.ts +11 -0
  265. package/src/modules/customer_accounts/backend/customer_accounts/[id]/page.tsx +607 -0
  266. package/src/modules/customer_accounts/backend/customer_accounts/roles/[id]/page.meta.ts +12 -0
  267. package/src/modules/customer_accounts/backend/customer_accounts/roles/[id]/page.tsx +385 -0
  268. package/src/modules/customer_accounts/backend/customer_accounts/roles/create/page.meta.ts +12 -0
  269. package/src/modules/customer_accounts/backend/customer_accounts/roles/create/page.tsx +203 -0
  270. package/src/modules/customer_accounts/backend/customer_accounts/roles/page.meta.ts +31 -0
  271. package/src/modules/customer_accounts/backend/customer_accounts/roles/page.tsx +217 -0
  272. package/src/modules/customer_accounts/backend/page.meta.ts +33 -0
  273. package/src/modules/customer_accounts/backend/page.tsx +535 -0
  274. package/src/modules/customer_accounts/ce.ts +22 -0
  275. package/src/modules/customer_accounts/data/enrichers.ts +117 -0
  276. package/src/modules/customer_accounts/data/entities.ts +302 -0
  277. package/src/modules/customer_accounts/data/extensions.ts +4 -0
  278. package/src/modules/customer_accounts/data/validators.ts +128 -0
  279. package/src/modules/customer_accounts/di.ts +15 -0
  280. package/src/modules/customer_accounts/events.ts +28 -0
  281. package/src/modules/customer_accounts/i18n/de.json +176 -0
  282. package/src/modules/customer_accounts/i18n/en.json +176 -0
  283. package/src/modules/customer_accounts/i18n/es.json +176 -0
  284. package/src/modules/customer_accounts/i18n/pl.json +176 -0
  285. package/src/modules/customer_accounts/index.ts +13 -0
  286. package/src/modules/customer_accounts/lib/customerAuth.ts +85 -0
  287. package/src/modules/customer_accounts/lib/customerAuthServer.ts +54 -0
  288. package/src/modules/customer_accounts/lib/rateLimiter.ts +36 -0
  289. package/src/modules/customer_accounts/lib/tokenGenerator.ts +9 -0
  290. package/src/modules/customer_accounts/migrations/.snapshot-open-mercato.json +1255 -0
  291. package/src/modules/customer_accounts/migrations/Migration20260313222043.ts +62 -0
  292. package/src/modules/customer_accounts/notifications.client.ts +46 -0
  293. package/src/modules/customer_accounts/notifications.ts +44 -0
  294. package/src/modules/customer_accounts/search.ts +134 -0
  295. package/src/modules/customer_accounts/services/customerInvitationService.ts +109 -0
  296. package/src/modules/customer_accounts/services/customerRbacService.ts +144 -0
  297. package/src/modules/customer_accounts/services/customerSessionService.ts +90 -0
  298. package/src/modules/customer_accounts/services/customerTokenService.ts +98 -0
  299. package/src/modules/customer_accounts/services/customerUserService.ts +105 -0
  300. package/src/modules/customer_accounts/setup.ts +212 -0
  301. package/src/modules/customer_accounts/subscribers/autoLinkCrm.ts +65 -0
  302. package/src/modules/customer_accounts/subscribers/autoLinkCrmReverse.ts +78 -0
  303. package/src/modules/customer_accounts/subscribers/notifyStaffOnSignup.ts +32 -0
  304. package/src/modules/customer_accounts/translations.ts +5 -0
  305. package/src/modules/customer_accounts/widgets/injection/account-status/widget.client.tsx +89 -0
  306. package/src/modules/customer_accounts/widgets/injection/account-status/widget.ts +16 -0
  307. package/src/modules/customer_accounts/widgets/injection/company-users/widget.client.tsx +78 -0
  308. package/src/modules/customer_accounts/widgets/injection/company-users/widget.ts +16 -0
  309. package/src/modules/customer_accounts/widgets/injection-table.ts +24 -0
  310. package/src/modules/customer_accounts/workers/cleanupExpiredSessions.ts +33 -0
  311. package/src/modules/customer_accounts/workers/cleanupExpiredTokens.ts +51 -0
  312. package/src/modules/customers/components/AddressTiles.tsx +1 -1
  313. package/src/modules/directory/api/get/organizations/lookup.ts +92 -0
  314. package/src/modules/directory/commands/organizations.ts +34 -1
  315. package/src/modules/directory/data/entities.ts +5 -1
  316. package/src/modules/directory/data/validators.ts +4 -0
  317. package/src/modules/directory/migrations/.snapshot-open-mercato.json +20 -1
  318. package/src/modules/directory/migrations/Migration20260314143323.ts +15 -0
  319. package/src/modules/directory/setup.ts +41 -0
  320. package/src/modules/payment_gateways/migrations/.snapshot-open-mercato.json +4 -1
  321. package/src/modules/payment_gateways/migrations/Migration20260313222043.ts +17 -0
  322. package/src/modules/portal/frontend/[orgSlug]/portal/dashboard/page.tsx +158 -0
  323. package/src/modules/portal/frontend/[orgSlug]/portal/login/page.tsx +120 -0
  324. package/src/modules/portal/frontend/[orgSlug]/portal/page.tsx +118 -0
  325. package/src/modules/portal/frontend/[orgSlug]/portal/profile/page.tsx +112 -0
  326. package/src/modules/portal/frontend/[orgSlug]/portal/signup/page.tsx +138 -0
  327. package/src/modules/portal/i18n/de.json +93 -0
  328. package/src/modules/portal/i18n/en.json +93 -0
  329. package/src/modules/portal/i18n/es.json +93 -0
  330. package/src/modules/portal/i18n/pl.json +93 -0
  331. package/src/modules/portal/index.ts +9 -0
  332. package/src/modules/portal/setup.ts +23 -0
  333. package/src/modules/shipping_carriers/migrations/.snapshot-open-mercato.json +226 -0
@@ -0,0 +1,158 @@
1
+ "use client"
2
+ import React, { useEffect, useMemo, useState, useCallback } from 'react'
3
+ import { useRouter } from 'next/navigation'
4
+ import { useT } from '@open-mercato/shared/lib/i18n/context'
5
+ import { Button } from '@open-mercato/ui/primitives/button'
6
+ import { Spinner } from '@open-mercato/ui/primitives/spinner'
7
+ import { usePortalContext } from '@open-mercato/ui/portal/PortalContext'
8
+ import { PortalPageHeader } from '@open-mercato/ui/portal/components/PortalPageHeader'
9
+ import { PortalCard, PortalCardHeader } from '@open-mercato/ui/portal/components/PortalCard'
10
+ import { PortalEmptyState } from '@open-mercato/ui/portal/components/PortalEmptyState'
11
+ import { usePortalDashboardWidgets } from '@open-mercato/ui/portal/hooks/usePortalDashboardWidgets'
12
+ import { InjectionSpot } from '@open-mercato/ui/backend/injection/InjectionSpot'
13
+ import { PortalInjectionSpots } from '@open-mercato/ui/backend/injection/spotIds'
14
+
15
+ type Props = { params: { orgSlug: string } }
16
+
17
+ const HIDDEN_WIDGETS_KEY = 'om:portal:dashboard:hidden'
18
+
19
+ function loadHiddenWidgets(): Set<string> {
20
+ try {
21
+ const raw = localStorage.getItem(HIDDEN_WIDGETS_KEY)
22
+ if (!raw) return new Set()
23
+ return new Set(JSON.parse(raw))
24
+ } catch {
25
+ return new Set()
26
+ }
27
+ }
28
+
29
+ function saveHiddenWidgets(hidden: Set<string>) {
30
+ try {
31
+ localStorage.setItem(HIDDEN_WIDGETS_KEY, JSON.stringify(Array.from(hidden)))
32
+ } catch {
33
+ // best effort
34
+ }
35
+ }
36
+
37
+ function WidgetIcon({ className }: { className?: string }) {
38
+ return (
39
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" className={className}>
40
+ <rect width="7" height="9" x="3" y="3" rx="1" /><rect width="7" height="5" x="14" y="3" rx="1" /><rect width="7" height="5" x="14" y="12" rx="1" /><rect width="7" height="9" x="3" y="16" rx="1" />
41
+ </svg>
42
+ )
43
+ }
44
+
45
+ export default function PortalDashboardPage({ params }: Props) {
46
+ const t = useT()
47
+ const router = useRouter()
48
+ const { auth } = usePortalContext()
49
+ const { user, loading } = auth
50
+
51
+ const [editing, setEditing] = useState(false)
52
+ const [hiddenWidgets, setHiddenWidgets] = useState<Set<string>>(() => loadHiddenWidgets())
53
+
54
+ const { widgets: dashboardWidgets, isLoading: widgetsLoading } = usePortalDashboardWidgets('portal:dashboard:sections' as any)
55
+
56
+ useEffect(() => {
57
+ if (!loading && !user) {
58
+ router.replace(`/${params.orgSlug}/portal/login`)
59
+ }
60
+ }, [loading, user, router, params.orgSlug])
61
+
62
+ const toggleWidget = useCallback((widgetId: string) => {
63
+ setHiddenWidgets((prev) => {
64
+ const next = new Set(prev)
65
+ if (next.has(widgetId)) {
66
+ next.delete(widgetId)
67
+ } else {
68
+ next.add(widgetId)
69
+ }
70
+ saveHiddenWidgets(next)
71
+ return next
72
+ })
73
+ }, [])
74
+
75
+ const visibleWidgets = useMemo(
76
+ () => dashboardWidgets.filter((w) => !hiddenWidgets.has(w.metadata.id)),
77
+ [dashboardWidgets, hiddenWidgets],
78
+ )
79
+
80
+ const injectionContext = useMemo(
81
+ () => ({ orgSlug: params.orgSlug, user, roles: auth.roles, resolvedFeatures: auth.resolvedFeatures }),
82
+ [params.orgSlug, user, auth.roles, auth.resolvedFeatures],
83
+ )
84
+
85
+ if (loading) {
86
+ return <div className="flex items-center justify-center py-20"><Spinner /></div>
87
+ }
88
+
89
+ if (!user) return null
90
+
91
+ return (
92
+ <div className="flex flex-col gap-8">
93
+ <PortalPageHeader
94
+ label={t('portal.dashboard.title', 'Dashboard')}
95
+ title={t('portal.dashboard.welcome', { name: user.displayName })}
96
+ action={
97
+ dashboardWidgets.length > 0 ? (
98
+ <Button
99
+ type="button"
100
+ variant={editing ? 'default' : 'outline'}
101
+ size="sm"
102
+ className="rounded-lg text-[13px]"
103
+ onClick={() => setEditing((prev) => !prev)}
104
+ >
105
+ {editing ? t('portal.dashboard.done', 'Done') : t('portal.dashboard.customize', 'Customize')}
106
+ </Button>
107
+ ) : null
108
+ }
109
+ />
110
+
111
+ <InjectionSpot spotId={PortalInjectionSpots.pageBefore('dashboard')} context={injectionContext} />
112
+
113
+ {editing && dashboardWidgets.length > 0 ? (
114
+ <PortalCard>
115
+ <PortalCardHeader
116
+ label={t('portal.dashboard.customize', 'Customize')}
117
+ title={t('portal.dashboard.widgets', 'Dashboard Widgets')}
118
+ />
119
+ <div className="flex flex-wrap gap-2">
120
+ {dashboardWidgets.map((widget) => {
121
+ const isHidden = hiddenWidgets.has(widget.metadata.id)
122
+ return (
123
+ <Button key={widget.metadata.id} type="button" variant={isHidden ? 'outline' : 'default'} size="sm" className="rounded-lg text-[13px]" onClick={() => toggleWidget(widget.metadata.id)}>
124
+ {widget.metadata.title || widget.metadata.id}
125
+ </Button>
126
+ )
127
+ })}
128
+ </div>
129
+ </PortalCard>
130
+ ) : null}
131
+
132
+ {visibleWidgets.length > 0 ? (
133
+ <div className="grid gap-5 md:grid-cols-2 xl:grid-cols-3">
134
+ {visibleWidgets.map((widget) => {
135
+ const WidgetComponent = widget.Widget
136
+ if (!WidgetComponent) return null
137
+ return (
138
+ <PortalCard key={widget.metadata.id}>
139
+ <PortalCardHeader title={widget.metadata.title || widget.metadata.id} />
140
+ <WidgetComponent context={{ orgSlug: params.orgSlug, user, roles: auth.roles, resolvedFeatures: auth.resolvedFeatures }} />
141
+ </PortalCard>
142
+ )
143
+ })}
144
+ </div>
145
+ ) : null}
146
+
147
+ {dashboardWidgets.length === 0 && !widgetsLoading ? (
148
+ <PortalEmptyState
149
+ icon={<WidgetIcon className="size-5" />}
150
+ title={t('portal.dashboard.emptyWidgets', 'No dashboard widgets yet')}
151
+ description="Modules can inject widgets into this dashboard via the portal:dashboard:sections injection spot."
152
+ />
153
+ ) : null}
154
+
155
+ <InjectionSpot spotId={PortalInjectionSpots.pageAfter('dashboard')} context={injectionContext} />
156
+ </div>
157
+ )
158
+ }
@@ -0,0 +1,120 @@
1
+ "use client"
2
+ import { useCallback, useMemo, useState } from 'react'
3
+ import Link from 'next/link'
4
+ import { useT } from '@open-mercato/shared/lib/i18n/context'
5
+ import { Input } from '@open-mercato/ui/primitives/input'
6
+ import { Label } from '@open-mercato/ui/primitives/label'
7
+ import { Button } from '@open-mercato/ui/primitives/button'
8
+ import { Notice } from '@open-mercato/ui/primitives/Notice'
9
+ import { Spinner } from '@open-mercato/ui/primitives/spinner'
10
+ import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
11
+ import { usePortalContext } from '@open-mercato/ui/portal/PortalContext'
12
+ import { InjectionSpot } from '@open-mercato/ui/backend/injection/InjectionSpot'
13
+ import { PortalInjectionSpots } from '@open-mercato/ui/backend/injection/spotIds'
14
+
15
+ type Props = { params: { orgSlug: string } }
16
+
17
+ export default function PortalLoginPage({ params }: Props) {
18
+ const t = useT()
19
+ const orgSlug = params.orgSlug
20
+ const { tenant } = usePortalContext()
21
+
22
+ const [email, setEmail] = useState('')
23
+ const [password, setPassword] = useState('')
24
+ const [error, setError] = useState<string | null>(null)
25
+ const [submitting, setSubmitting] = useState(false)
26
+
27
+ const handleSubmit = useCallback(
28
+ async (event: React.FormEvent) => {
29
+ event.preventDefault()
30
+ setError(null)
31
+
32
+ if (!tenant.tenantId) {
33
+ setError(t('portal.org.invalid', 'Organization not found.'))
34
+ return
35
+ }
36
+
37
+ setSubmitting(true)
38
+ try {
39
+ const result = await apiCall<{ ok: boolean; error?: string }>('/api/customer_accounts/login', {
40
+ method: 'POST',
41
+ headers: { 'Content-Type': 'application/json' },
42
+ body: JSON.stringify({ email, password, tenantId: tenant.tenantId }),
43
+ })
44
+
45
+ if (result.ok && result.result?.ok) {
46
+ window.location.assign(`/${orgSlug}/portal/dashboard`)
47
+ return
48
+ }
49
+
50
+ if (result.status === 423) {
51
+ setError(t('portal.login.error.locked', 'Account locked. Try again later.'))
52
+ } else if (result.status === 401) {
53
+ setError(t('portal.login.error.invalidCredentials', 'Invalid email or password.'))
54
+ } else {
55
+ setError(result.result?.error || t('portal.login.error.generic', 'Login failed. Please try again.'))
56
+ }
57
+ } catch {
58
+ setError(t('portal.login.error.generic', 'Login failed. Please try again.'))
59
+ } finally {
60
+ setSubmitting(false)
61
+ }
62
+ },
63
+ [email, password, tenant.tenantId, orgSlug, t],
64
+ )
65
+
66
+ const injectionContext = useMemo(
67
+ () => ({ orgSlug }),
68
+ [orgSlug],
69
+ )
70
+
71
+ if (tenant.loading) {
72
+ return <div className="flex items-center justify-center py-20"><Spinner /></div>
73
+ }
74
+
75
+ if (tenant.error) {
76
+ return (
77
+ <div className="mx-auto w-full max-w-md py-12">
78
+ <Notice variant="error">{t('portal.org.invalid', 'Organization not found.')}</Notice>
79
+ </div>
80
+ )
81
+ }
82
+
83
+ return (
84
+ <div className="mx-auto w-full max-w-sm">
85
+ <div className="mb-8 text-center">
86
+ <h1 className="text-2xl font-bold tracking-tight">{t('portal.login.title', 'Sign In')}</h1>
87
+ <p className="mt-1.5 text-sm text-muted-foreground">{t('portal.login.description', 'Enter your credentials to access the portal.')}</p>
88
+ </div>
89
+
90
+ <InjectionSpot spotId={PortalInjectionSpots.pageBefore('login')} context={injectionContext} />
91
+
92
+ <form onSubmit={handleSubmit} className="flex flex-col gap-4">
93
+ {error ? <Notice variant="error">{error}</Notice> : null}
94
+
95
+ <div className="flex flex-col gap-1.5">
96
+ <Label htmlFor="login-email" className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground/70">{t('portal.login.email', 'Email')}</Label>
97
+ <Input id="login-email" type="email" autoComplete="email" required placeholder={t('portal.login.email.placeholder', 'you@example.com')} value={email} onChange={(e) => setEmail(e.target.value)} disabled={submitting} className="rounded-lg" />
98
+ </div>
99
+
100
+ <div className="flex flex-col gap-1.5">
101
+ <Label htmlFor="login-password" className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground/70">{t('portal.login.password', 'Password')}</Label>
102
+ <Input id="login-password" type="password" autoComplete="current-password" required placeholder={t('portal.login.password.placeholder', '\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022')} value={password} onChange={(e) => setPassword(e.target.value)} disabled={submitting} className="rounded-lg" />
103
+ </div>
104
+
105
+ <Button type="submit" disabled={submitting} className="mt-1 w-full rounded-lg">
106
+ {submitting ? t('portal.login.submitting', 'Signing in...') : t('portal.login.submit', 'Sign In')}
107
+ </Button>
108
+
109
+ <p className="text-center text-[13px] text-muted-foreground">
110
+ {t('portal.login.noAccount', "Don't have an account?")}{' '}
111
+ <Link href={`/${orgSlug}/portal/signup`} className="font-medium text-foreground underline underline-offset-4 hover:opacity-80">
112
+ {t('portal.login.signupLink', 'Sign up')}
113
+ </Link>
114
+ </p>
115
+ </form>
116
+
117
+ <InjectionSpot spotId={PortalInjectionSpots.pageAfter('login')} context={injectionContext} />
118
+ </div>
119
+ )
120
+ }
@@ -0,0 +1,118 @@
1
+ "use client"
2
+ import { useEffect, useMemo } from 'react'
3
+ import Link from 'next/link'
4
+ import { useRouter } from 'next/navigation'
5
+ import { useT } from '@open-mercato/shared/lib/i18n/context'
6
+ import { Button } from '@open-mercato/ui/primitives/button'
7
+ import { Spinner } from '@open-mercato/ui/primitives/spinner'
8
+ import { Notice } from '@open-mercato/ui/primitives/Notice'
9
+ import { usePortalContext } from '@open-mercato/ui/portal/PortalContext'
10
+ import { PortalFeatureCard } from '@open-mercato/ui/portal/components/PortalFeatureCard'
11
+ import { InjectionSpot } from '@open-mercato/ui/backend/injection/InjectionSpot'
12
+ import { PortalInjectionSpots } from '@open-mercato/ui/backend/injection/spotIds'
13
+
14
+ type Props = { params: { orgSlug: string } }
15
+
16
+ function ShoppingBagIcon({ className }: { className?: string }) {
17
+ return (
18
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" className={className}>
19
+ <path d="M6 2 3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4Z" /><line x1="3" x2="21" y1="6" y2="6" /><path d="M16 10a4 4 0 0 1-8 0" />
20
+ </svg>
21
+ )
22
+ }
23
+
24
+ function UserIcon({ className }: { className?: string }) {
25
+ return (
26
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" className={className}>
27
+ <path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2" /><circle cx="12" cy="7" r="4" />
28
+ </svg>
29
+ )
30
+ }
31
+
32
+ function ShieldIcon({ className }: { className?: string }) {
33
+ return (
34
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" className={className}>
35
+ <path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z" />
36
+ </svg>
37
+ )
38
+ }
39
+
40
+ export default function PortalLandingPage({ params }: Props) {
41
+ const t = useT()
42
+ const router = useRouter()
43
+ const orgSlug = params.orgSlug
44
+ const { auth, tenant } = usePortalContext()
45
+
46
+ // Redirect authenticated users to dashboard
47
+ useEffect(() => {
48
+ if (!auth.loading && auth.user) {
49
+ router.replace(`/${orgSlug}/portal/dashboard`)
50
+ }
51
+ }, [auth.loading, auth.user, router, orgSlug])
52
+
53
+ const injectionContext = useMemo(
54
+ () => ({ orgSlug }),
55
+ [orgSlug],
56
+ )
57
+
58
+ if (auth.loading || tenant.loading) {
59
+ return <div className="flex items-center justify-center py-20"><Spinner /></div>
60
+ }
61
+
62
+ if (tenant.error) {
63
+ return (
64
+ <div className="mx-auto w-full max-w-md py-12">
65
+ <Notice variant="error">{t('portal.org.invalid', 'Organization not found.')}</Notice>
66
+ </div>
67
+ )
68
+ }
69
+
70
+ // Authenticated user — redirect is in progress
71
+ if (auth.user) return null
72
+
73
+ return (
74
+ <>
75
+ <InjectionSpot spotId={PortalInjectionSpots.pageBefore('home')} context={injectionContext} />
76
+
77
+ <section className="flex flex-col items-center gap-5 py-8 text-center sm:py-16">
78
+ <p className="text-[11px] font-semibold uppercase tracking-[0.15em] text-muted-foreground/60">
79
+ {t('portal.nav.home', 'Customer Portal')}
80
+ </p>
81
+ <h1 className="max-w-2xl text-4xl font-bold tracking-tight text-foreground sm:text-5xl lg:text-6xl">
82
+ {t('portal.landing.hero.title', 'Welcome to your portal')}
83
+ </h1>
84
+ <p className="max-w-lg text-base leading-relaxed text-muted-foreground sm:text-lg">
85
+ {t('portal.landing.hero.description', 'Access your account, manage orders, and stay up to date.')}
86
+ </p>
87
+ <div className="mt-2 flex items-center gap-3">
88
+ <Button asChild size="lg" className="rounded-lg px-6 text-[14px]">
89
+ <Link href={`/${orgSlug}/portal/login`}>{t('portal.landing.cta.login', 'Sign In')}</Link>
90
+ </Button>
91
+ <Button asChild variant="outline" size="lg" className="rounded-lg px-6 text-[14px]">
92
+ <Link href={`/${orgSlug}/portal/signup`}>{t('portal.landing.cta.signup', 'Create Account')}</Link>
93
+ </Button>
94
+ </div>
95
+ </section>
96
+
97
+ <section className="grid gap-4 sm:grid-cols-3">
98
+ <PortalFeatureCard
99
+ icon={<ShoppingBagIcon className="size-5" />}
100
+ title={t('portal.landing.feature.orders', 'Orders & Invoices')}
101
+ description={t('portal.landing.feature.orders.description', 'Track your orders, download invoices, and view delivery status in real time.')}
102
+ />
103
+ <PortalFeatureCard
104
+ icon={<UserIcon className="size-5" />}
105
+ title={t('portal.landing.feature.account', 'Account Management')}
106
+ description={t('portal.landing.feature.account.description', 'Update your profile, manage team members, and configure your preferences.')}
107
+ />
108
+ <PortalFeatureCard
109
+ icon={<ShieldIcon className="size-5" />}
110
+ title={t('portal.landing.feature.security', 'Secure Access')}
111
+ description={t('portal.landing.feature.security.description', 'Role-based permissions, session management, and full audit trail.')}
112
+ />
113
+ </section>
114
+
115
+ <InjectionSpot spotId={PortalInjectionSpots.pageAfter('home')} context={injectionContext} />
116
+ </>
117
+ )
118
+ }
@@ -0,0 +1,112 @@
1
+ "use client"
2
+ import { useEffect, useMemo } from 'react'
3
+ import { useRouter } from 'next/navigation'
4
+ import { useT } from '@open-mercato/shared/lib/i18n/context'
5
+ import { Badge } from '@open-mercato/ui/primitives/badge'
6
+ import { Spinner } from '@open-mercato/ui/primitives/spinner'
7
+ import { usePortalContext } from '@open-mercato/ui/portal/PortalContext'
8
+ import { PortalPageHeader } from '@open-mercato/ui/portal/components/PortalPageHeader'
9
+ import { PortalCard, PortalCardHeader, PortalStatRow, PortalCardDivider } from '@open-mercato/ui/portal/components/PortalCard'
10
+ import { InjectionSpot } from '@open-mercato/ui/backend/injection/InjectionSpot'
11
+ import { PortalInjectionSpots } from '@open-mercato/ui/backend/injection/spotIds'
12
+
13
+ type Props = { params: { orgSlug: string } }
14
+
15
+ export default function PortalProfilePage({ params }: Props) {
16
+ const t = useT()
17
+ const router = useRouter()
18
+ const { auth } = usePortalContext()
19
+ const { user, roles, resolvedFeatures, isPortalAdmin, loading } = auth
20
+
21
+ useEffect(() => {
22
+ if (!loading && !user) {
23
+ router.replace(`/${params.orgSlug}/portal/login`)
24
+ }
25
+ }, [loading, user, router, params.orgSlug])
26
+
27
+ const injectionContext = useMemo(
28
+ () => ({ orgSlug: params.orgSlug, user, roles, resolvedFeatures, isPortalAdmin }),
29
+ [params.orgSlug, user, roles, resolvedFeatures, isPortalAdmin],
30
+ )
31
+
32
+ if (loading) {
33
+ return <div className="flex items-center justify-center py-20"><Spinner /></div>
34
+ }
35
+
36
+ if (!user) return null
37
+
38
+ const formatDate = (dateString: string | null) => {
39
+ if (!dateString) return t('portal.dashboard.never', 'Never')
40
+ return new Date(dateString).toLocaleDateString(undefined, {
41
+ year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit',
42
+ })
43
+ }
44
+
45
+ return (
46
+ <div className="flex flex-col gap-8">
47
+ <PortalPageHeader
48
+ label={t('portal.profile.label', 'Account')}
49
+ title={t('portal.profile.title', 'Profile')}
50
+ />
51
+
52
+ <InjectionSpot spotId={PortalInjectionSpots.pageBefore('profile')} context={injectionContext} />
53
+
54
+ <div className="grid gap-5 md:grid-cols-2">
55
+ <PortalCard>
56
+ <PortalCardHeader label={t('portal.dashboard.profile', 'Profile')} title={user.displayName} />
57
+ <div className="flex flex-col">
58
+ <PortalStatRow
59
+ label={t('portal.dashboard.email', 'Email')}
60
+ value={
61
+ <span className="flex items-center gap-2">
62
+ <span className="truncate">{user.email}</span>
63
+ <Badge variant={user.emailVerified ? 'default' : 'outline'} className="shrink-0 text-[10px]">
64
+ {user.emailVerified ? t('portal.dashboard.emailVerified', 'Verified') : t('portal.dashboard.emailNotVerified', 'Unverified')}
65
+ </Badge>
66
+ </span>
67
+ }
68
+ />
69
+ <PortalCardDivider />
70
+ <PortalStatRow label={t('portal.dashboard.lastLogin', 'Last login')} value={formatDate(user.lastLoginAt)} />
71
+ <PortalCardDivider />
72
+ <PortalStatRow label={t('portal.dashboard.memberSince', 'Member since')} value={formatDate(user.createdAt)} />
73
+ {isPortalAdmin ? (
74
+ <>
75
+ <PortalCardDivider />
76
+ <PortalStatRow label={t('portal.dashboard.roles', 'Role')} value={<Badge className="text-[10px]">{t('portal.dashboard.portalAdmin', 'Portal Admin')}</Badge>} />
77
+ </>
78
+ ) : null}
79
+ </div>
80
+ </PortalCard>
81
+
82
+ <PortalCard>
83
+ <PortalCardHeader label={t('portal.dashboard.roles', 'Roles')} title={`${roles.length} assigned`} />
84
+ {roles.length > 0 ? (
85
+ <div className="flex flex-wrap gap-2">
86
+ {roles.map((role) => (
87
+ <span key={role.id} className="inline-flex items-center rounded-lg border px-3 py-1.5 text-[12px] font-medium">{role.name}</span>
88
+ ))}
89
+ </div>
90
+ ) : (
91
+ <p className="text-sm text-muted-foreground">{t('portal.dashboard.noRoles', 'No roles assigned')}</p>
92
+ )}
93
+ </PortalCard>
94
+
95
+ <PortalCard className="md:col-span-2">
96
+ <PortalCardHeader label={t('portal.dashboard.permissions', 'Permissions')} title={`${resolvedFeatures.length} features`} />
97
+ {resolvedFeatures.length > 0 ? (
98
+ <div className="flex flex-wrap gap-2">
99
+ {resolvedFeatures.map((feature) => (
100
+ <span key={feature} className="inline-flex items-center rounded-md border bg-muted/50 px-2 py-1 font-mono text-[11px] text-muted-foreground">{feature}</span>
101
+ ))}
102
+ </div>
103
+ ) : (
104
+ <p className="text-sm text-muted-foreground">{t('portal.dashboard.noPermissions', 'No permissions')}</p>
105
+ )}
106
+ </PortalCard>
107
+ </div>
108
+
109
+ <InjectionSpot spotId={PortalInjectionSpots.pageAfter('profile')} context={injectionContext} />
110
+ </div>
111
+ )
112
+ }
@@ -0,0 +1,138 @@
1
+ "use client"
2
+ import { useCallback, useMemo, useState } from 'react'
3
+ import Link from 'next/link'
4
+ import { useT } from '@open-mercato/shared/lib/i18n/context'
5
+ import { Input } from '@open-mercato/ui/primitives/input'
6
+ import { Label } from '@open-mercato/ui/primitives/label'
7
+ import { Button } from '@open-mercato/ui/primitives/button'
8
+ import { Notice } from '@open-mercato/ui/primitives/Notice'
9
+ import { Spinner } from '@open-mercato/ui/primitives/spinner'
10
+ import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
11
+ import { usePortalContext } from '@open-mercato/ui/portal/PortalContext'
12
+ import { InjectionSpot } from '@open-mercato/ui/backend/injection/InjectionSpot'
13
+ import { PortalInjectionSpots } from '@open-mercato/ui/backend/injection/spotIds'
14
+
15
+ type Props = { params: { orgSlug: string } }
16
+
17
+ export default function PortalSignupPage({ params }: Props) {
18
+ const t = useT()
19
+ const orgSlug = params.orgSlug
20
+ const { tenant } = usePortalContext()
21
+
22
+ const [displayName, setDisplayName] = useState('')
23
+ const [email, setEmail] = useState('')
24
+ const [password, setPassword] = useState('')
25
+ const [error, setError] = useState<string | null>(null)
26
+ const [success, setSuccess] = useState(false)
27
+ const [submitting, setSubmitting] = useState(false)
28
+
29
+ const handleSubmit = useCallback(
30
+ async (event: React.FormEvent) => {
31
+ event.preventDefault()
32
+ setError(null)
33
+
34
+ if (!tenant.tenantId || !tenant.organizationId) {
35
+ setError(t('portal.org.invalid', 'Organization not found.'))
36
+ return
37
+ }
38
+
39
+ setSubmitting(true)
40
+ try {
41
+ const result = await apiCall<{ ok: boolean; error?: string }>('/api/customer_accounts/signup', {
42
+ method: 'POST',
43
+ headers: { 'Content-Type': 'application/json' },
44
+ body: JSON.stringify({ email, password, displayName, tenantId: tenant.tenantId, organizationId: tenant.organizationId }),
45
+ })
46
+
47
+ if (result.status === 201 && result.result?.ok) {
48
+ setSuccess(true)
49
+ return
50
+ }
51
+
52
+ setError(result.result?.error || t('portal.signup.error.generic', 'Signup failed. Please try again.'))
53
+ } catch {
54
+ setError(t('portal.signup.error.generic', 'Signup failed. Please try again.'))
55
+ } finally {
56
+ setSubmitting(false)
57
+ }
58
+ },
59
+ [displayName, email, password, tenant.tenantId, tenant.organizationId, t],
60
+ )
61
+
62
+ const injectionContext = useMemo(
63
+ () => ({ orgSlug }),
64
+ [orgSlug],
65
+ )
66
+
67
+ if (tenant.loading) {
68
+ return <div className="flex items-center justify-center py-20"><Spinner /></div>
69
+ }
70
+
71
+ if (tenant.error) {
72
+ return (
73
+ <div className="mx-auto w-full max-w-md py-12">
74
+ <Notice variant="error">{t('portal.org.invalid', 'Organization not found.')}</Notice>
75
+ </div>
76
+ )
77
+ }
78
+
79
+ if (success) {
80
+ return (
81
+ <div className="mx-auto w-full max-w-sm text-center">
82
+ <div className="mx-auto mb-4 flex size-12 items-center justify-center rounded-full bg-foreground text-background">
83
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="size-6">
84
+ <polyline points="20 6 9 17 4 12" />
85
+ </svg>
86
+ </div>
87
+ <h1 className="text-2xl font-bold tracking-tight">{t('portal.signup.success.title', 'Account Created')}</h1>
88
+ <p className="mt-1.5 text-sm text-muted-foreground">{t('portal.signup.success.description', 'Your account has been created. You can now sign in.')}</p>
89
+ <Button asChild className="mt-6 w-full rounded-lg">
90
+ <Link href={`/${orgSlug}/portal/login`}>{t('portal.signup.success.loginLink', 'Sign In')}</Link>
91
+ </Button>
92
+ </div>
93
+ )
94
+ }
95
+
96
+ return (
97
+ <div className="mx-auto w-full max-w-sm">
98
+ <div className="mb-8 text-center">
99
+ <h1 className="text-2xl font-bold tracking-tight">{t('portal.signup.title', 'Create Account')}</h1>
100
+ <p className="mt-1.5 text-sm text-muted-foreground">{t('portal.signup.description', 'Sign up for a portal account.')}</p>
101
+ </div>
102
+
103
+ <InjectionSpot spotId={PortalInjectionSpots.pageBefore('signup')} context={injectionContext} />
104
+
105
+ <form onSubmit={handleSubmit} className="flex flex-col gap-4">
106
+ {error ? <Notice variant="error">{error}</Notice> : null}
107
+
108
+ <div className="flex flex-col gap-1.5">
109
+ <Label htmlFor="signup-name" className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground/70">{t('portal.signup.displayName', 'Full Name')}</Label>
110
+ <Input id="signup-name" type="text" autoComplete="name" required placeholder={t('portal.signup.displayName.placeholder', 'Jane Smith')} value={displayName} onChange={(e) => setDisplayName(e.target.value)} disabled={submitting} className="rounded-lg" />
111
+ </div>
112
+
113
+ <div className="flex flex-col gap-1.5">
114
+ <Label htmlFor="signup-email" className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground/70">{t('portal.signup.email', 'Email')}</Label>
115
+ <Input id="signup-email" type="email" autoComplete="email" required placeholder={t('portal.signup.email.placeholder', 'you@example.com')} value={email} onChange={(e) => setEmail(e.target.value)} disabled={submitting} className="rounded-lg" />
116
+ </div>
117
+
118
+ <div className="flex flex-col gap-1.5">
119
+ <Label htmlFor="signup-password" className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground/70">{t('portal.signup.password', 'Password')}</Label>
120
+ <Input id="signup-password" type="password" autoComplete="new-password" required placeholder={t('portal.signup.password.placeholder', '\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022')} value={password} onChange={(e) => setPassword(e.target.value)} disabled={submitting} className="rounded-lg" />
121
+ </div>
122
+
123
+ <Button type="submit" disabled={submitting} className="mt-1 w-full rounded-lg">
124
+ {submitting ? t('portal.signup.submitting', 'Creating account...') : t('portal.signup.submit', 'Create Account')}
125
+ </Button>
126
+
127
+ <p className="text-center text-[13px] text-muted-foreground">
128
+ {t('portal.signup.hasAccount', 'Already have an account?')}{' '}
129
+ <Link href={`/${orgSlug}/portal/login`} className="font-medium text-foreground underline underline-offset-4 hover:opacity-80">
130
+ {t('portal.signup.loginLink', 'Sign in')}
131
+ </Link>
132
+ </p>
133
+ </form>
134
+
135
+ <InjectionSpot spotId={PortalInjectionSpots.pageAfter('signup')} context={injectionContext} />
136
+ </div>
137
+ )
138
+ }