@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,11 @@
1
+ export const metadata = {
2
+ requireAuth: true,
3
+ requireFeatures: ['customer_accounts.view'],
4
+ pageTitle: 'Customer User Detail',
5
+ pageTitleKey: 'customer_accounts.nav.user_detail',
6
+ navHidden: true,
7
+ breadcrumb: [
8
+ { label: 'Customer Accounts', labelKey: 'customer_accounts.nav.users', href: '/backend/customer_accounts' },
9
+ { label: 'User Detail', labelKey: 'customer_accounts.nav.user_detail' },
10
+ ],
11
+ } as const
@@ -0,0 +1,607 @@
1
+ "use client"
2
+
3
+ import * as React from 'react'
4
+ import Link from 'next/link'
5
+ import { useRouter } from 'next/navigation'
6
+ import { Page, PageBody } from '@open-mercato/ui/backend/Page'
7
+ import { FormHeader } from '@open-mercato/ui/backend/forms'
8
+ import { Button } from '@open-mercato/ui/primitives/button'
9
+ import { Spinner } from '@open-mercato/ui/primitives/spinner'
10
+ import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@open-mercato/ui/primitives/dialog'
11
+ import { apiCall, readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'
12
+ import { flash } from '@open-mercato/ui/backend/FlashMessages'
13
+ import { useT } from '@open-mercato/shared/lib/i18n/context'
14
+ import { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'
15
+ import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
16
+
17
+ type UserDetail = {
18
+ id: string
19
+ displayName: string
20
+ email: string
21
+ emailVerifiedAt: string | null
22
+ isActive: boolean
23
+ lastLoginAt: string | null
24
+ personEntityId: string | null
25
+ customerEntityId: string | null
26
+ createdAt: string
27
+ updatedAt: string | null
28
+ roles: Array<{ id: string; name: string; slug: string }>
29
+ sessions: Array<{
30
+ id: string
31
+ ipAddress: string | null
32
+ userAgent: string | null
33
+ lastUsedAt: string | null
34
+ createdAt: string
35
+ expiresAt: string
36
+ }>
37
+ }
38
+
39
+ function formatDate(value: string | null | undefined, fallback: string): string {
40
+ if (!value) return fallback
41
+ const date = new Date(value)
42
+ if (Number.isNaN(date.getTime())) return fallback
43
+ return date.toLocaleString()
44
+ }
45
+
46
+ function ResetPasswordDialog({
47
+ open,
48
+ onOpenChange,
49
+ userId,
50
+ onRunMutation,
51
+ }: {
52
+ open: boolean
53
+ onOpenChange: (next: boolean) => void
54
+ userId: string
55
+ onRunMutation: <T>(operation: () => Promise<T>) => Promise<T>
56
+ }) {
57
+ const t = useT()
58
+ const [newPassword, setNewPassword] = React.useState('')
59
+ const [isSubmitting, setIsSubmitting] = React.useState(false)
60
+
61
+ const handleSubmit = React.useCallback(async (event: React.FormEvent) => {
62
+ event.preventDefault()
63
+ if (!newPassword.trim() || newPassword.length < 8) {
64
+ flash(t('customer_accounts.admin.detail.resetPassword.error.minLength', 'Password must be at least 8 characters'), 'error')
65
+ return
66
+ }
67
+ setIsSubmitting(true)
68
+ try {
69
+ await onRunMutation(async () => {
70
+ const call = await apiCall<{ ok: boolean; error?: string }>(
71
+ `/api/customer_accounts/admin/users/${encodeURIComponent(userId)}/reset-password`,
72
+ {
73
+ method: 'POST',
74
+ headers: { 'content-type': 'application/json' },
75
+ body: JSON.stringify({ newPassword }),
76
+ },
77
+ )
78
+ if (!call.ok) {
79
+ flash(call.result?.error || t('customer_accounts.admin.detail.resetPassword.error.save', 'Failed to reset password'), 'error')
80
+ return
81
+ }
82
+ flash(t('customer_accounts.admin.detail.resetPassword.flash.success', 'Password reset successfully'), 'success')
83
+ setNewPassword('')
84
+ onOpenChange(false)
85
+ })
86
+ } catch (err) {
87
+ const message = err instanceof Error ? err.message : t('customer_accounts.admin.detail.resetPassword.error.save', 'Failed to reset password')
88
+ flash(message, 'error')
89
+ } finally {
90
+ setIsSubmitting(false)
91
+ }
92
+ }, [newPassword, onOpenChange, onRunMutation, t, userId])
93
+
94
+ const handleKeyDown = React.useCallback((event: React.KeyboardEvent) => {
95
+ if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {
96
+ event.preventDefault()
97
+ const form = (event.target as HTMLElement).closest('form')
98
+ if (form) form.requestSubmit()
99
+ }
100
+ }, [])
101
+
102
+ return (
103
+ <Dialog open={open} onOpenChange={(next) => { if (!next) setNewPassword(''); onOpenChange(next) }}>
104
+ <DialogContent className="sm:max-w-md">
105
+ <DialogHeader>
106
+ <DialogTitle>{t('customer_accounts.admin.detail.resetPassword.title', 'Reset Password')}</DialogTitle>
107
+ </DialogHeader>
108
+ <form onSubmit={(event) => { void handleSubmit(event) }} onKeyDown={handleKeyDown} className="space-y-4">
109
+ <div className="space-y-2">
110
+ <label className="text-sm font-medium" htmlFor="reset-password">
111
+ {t('customer_accounts.admin.detail.resetPassword.fields.newPassword', 'New Password')}
112
+ </label>
113
+ <input
114
+ id="reset-password"
115
+ type="password"
116
+ required
117
+ minLength={8}
118
+ value={newPassword}
119
+ onChange={(event) => setNewPassword(event.target.value)}
120
+ className="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
121
+ placeholder={t('customer_accounts.admin.detail.resetPassword.fields.placeholder', 'Min. 8 characters')}
122
+ />
123
+ </div>
124
+ <div className="flex justify-end gap-2 pt-2">
125
+ <Button type="button" variant="outline" onClick={() => { setNewPassword(''); onOpenChange(false) }}>
126
+ {t('customer_accounts.admin.detail.resetPassword.actions.cancel', 'Cancel')}
127
+ </Button>
128
+ <Button type="submit" disabled={isSubmitting}>
129
+ {isSubmitting
130
+ ? t('customer_accounts.admin.detail.resetPassword.actions.resetting', 'Resetting...')
131
+ : t('customer_accounts.admin.detail.resetPassword.actions.reset', 'Reset Password')}
132
+ </Button>
133
+ </div>
134
+ </form>
135
+ </DialogContent>
136
+ </Dialog>
137
+ )
138
+ }
139
+
140
+ export default function CustomerUserDetailPage({ params }: { params?: { id?: string } }) {
141
+ const id = params?.id
142
+ const t = useT()
143
+ const router = useRouter()
144
+ const { confirm, ConfirmDialogElement } = useConfirmDialog()
145
+ const [data, setData] = React.useState<UserDetail | null>(null)
146
+ const [isLoading, setIsLoading] = React.useState(true)
147
+ const [error, setError] = React.useState<string | null>(null)
148
+ const [isSaving, setIsSaving] = React.useState(false)
149
+ const [editActive, setEditActive] = React.useState<boolean | null>(null)
150
+ const [editDisplayName, setEditDisplayName] = React.useState('')
151
+ const [availableRoles, setAvailableRoles] = React.useState<Array<{ id: string; name: string }>>([])
152
+ const [selectedRoleIds, setSelectedRoleIds] = React.useState<string[]>([])
153
+ const [resetPasswordOpen, setResetPasswordOpen] = React.useState(false)
154
+ const [isVerifying, setIsVerifying] = React.useState(false)
155
+
156
+ const mutationContextId = `customer_accounts:user:${id ?? 'pending'}`
157
+ const { runMutation, retryLastMutation } = useGuardedMutation<{
158
+ entityType: string
159
+ entityId?: string
160
+ }>({
161
+ contextId: mutationContextId,
162
+ })
163
+
164
+ const runMutationWithContext = React.useCallback(
165
+ async <T,>(operation: () => Promise<T>, mutationPayload?: Record<string, unknown>): Promise<T> => {
166
+ return runMutation({
167
+ operation,
168
+ mutationPayload,
169
+ context: { entityType: 'customer_accounts:user', entityId: id },
170
+ })
171
+ },
172
+ [id, runMutation],
173
+ )
174
+
175
+ React.useEffect(() => {
176
+ if (!id) {
177
+ setError(t('customer_accounts.admin.detail.error.notFound', 'User not found'))
178
+ setIsLoading(false)
179
+ return
180
+ }
181
+ let cancelled = false
182
+ async function load() {
183
+ setIsLoading(true)
184
+ setError(null)
185
+ try {
186
+ const payload = await readApiResultOrThrow<UserDetail>(
187
+ `/api/customer_accounts/admin/users/${encodeURIComponent(id!)}`,
188
+ undefined,
189
+ { errorMessage: t('customer_accounts.admin.detail.error.load', 'Failed to load user') },
190
+ )
191
+ if (cancelled) return
192
+ setData(payload)
193
+ setEditActive(payload.isActive)
194
+ setEditDisplayName(payload.displayName)
195
+ setSelectedRoleIds(payload.roles.map((role) => role.id))
196
+ } catch (err) {
197
+ if (cancelled) return
198
+ const message = err instanceof Error ? err.message : t('customer_accounts.admin.detail.error.load', 'Failed to load user')
199
+ setError(message)
200
+ } finally {
201
+ if (!cancelled) setIsLoading(false)
202
+ }
203
+ }
204
+ load()
205
+ return () => { cancelled = true }
206
+ }, [id, t])
207
+
208
+ React.useEffect(() => {
209
+ let cancelled = false
210
+ async function loadRoles() {
211
+ try {
212
+ const call = await apiCall<{ items?: Array<{ id: string; name: string }> }>(
213
+ '/api/customer_accounts/admin/roles?pageSize=100',
214
+ )
215
+ if (cancelled || !call.ok) return
216
+ const items = Array.isArray(call.result?.items) ? call.result!.items : []
217
+ setAvailableRoles(
218
+ items.filter((item) => typeof item?.id === 'string' && typeof item?.name === 'string'),
219
+ )
220
+ } catch {
221
+ // silently ignore role loading failures
222
+ }
223
+ }
224
+ loadRoles()
225
+ return () => { cancelled = true }
226
+ }, [])
227
+
228
+ const handleSave = React.useCallback(async () => {
229
+ if (!data || !id) return
230
+ setIsSaving(true)
231
+ try {
232
+ await runMutationWithContext(async () => {
233
+ const call = await apiCall<{ ok: boolean; error?: string }>(
234
+ `/api/customer_accounts/admin/users/${encodeURIComponent(id)}`,
235
+ {
236
+ method: 'PUT',
237
+ headers: { 'content-type': 'application/json' },
238
+ body: JSON.stringify({
239
+ displayName: editDisplayName.trim() || undefined,
240
+ isActive: editActive,
241
+ roleIds: selectedRoleIds,
242
+ }),
243
+ },
244
+ )
245
+ if (!call.ok) {
246
+ flash(call.result?.error || t('customer_accounts.admin.detail.error.save', 'Failed to save user'), 'error')
247
+ return
248
+ }
249
+ flash(t('customer_accounts.admin.detail.flash.saved', 'User updated'), 'success')
250
+ setData((prev) => prev ? {
251
+ ...prev,
252
+ isActive: editActive ?? prev.isActive,
253
+ displayName: editDisplayName.trim() || prev.displayName,
254
+ } : prev)
255
+ }, { displayName: editDisplayName, isActive: editActive, roleIds: selectedRoleIds })
256
+ } catch (err) {
257
+ const message = err instanceof Error ? err.message : t('customer_accounts.admin.detail.error.save', 'Failed to save user')
258
+ flash(message, 'error')
259
+ } finally {
260
+ setIsSaving(false)
261
+ }
262
+ }, [data, editActive, editDisplayName, id, runMutationWithContext, selectedRoleIds, t])
263
+
264
+ const handleDelete = React.useCallback(async () => {
265
+ if (!data || !id) return
266
+ const confirmed = await confirm({
267
+ title: t('customer_accounts.admin.confirm.delete', 'Delete user "{{name}}"?', {
268
+ name: data.displayName || data.email,
269
+ }),
270
+ variant: 'destructive',
271
+ })
272
+ if (!confirmed) return
273
+ try {
274
+ await runMutationWithContext(async () => {
275
+ const call = await apiCall(
276
+ `/api/customer_accounts/admin/users/${encodeURIComponent(id)}`,
277
+ { method: 'DELETE' },
278
+ )
279
+ if (!call.ok) {
280
+ flash(t('customer_accounts.admin.error.delete', 'Failed to delete user'), 'error')
281
+ return
282
+ }
283
+ flash(t('customer_accounts.admin.flash.deleted', 'User deleted'), 'success')
284
+ router.push('/backend/customer_accounts')
285
+ }, { id })
286
+ } catch (err) {
287
+ const message = err instanceof Error ? err.message : t('customer_accounts.admin.error.delete', 'Failed to delete user')
288
+ flash(message, 'error')
289
+ }
290
+ }, [confirm, data, id, router, runMutationWithContext, t])
291
+
292
+ const handleVerifyEmail = React.useCallback(async () => {
293
+ if (!data || !id) return
294
+ const confirmed = await confirm({
295
+ title: t('customer_accounts.admin.detail.verifyEmail.confirm', 'Mark email as verified for "{{name}}"?', {
296
+ name: data.displayName || data.email,
297
+ }),
298
+ })
299
+ if (!confirmed) return
300
+ setIsVerifying(true)
301
+ try {
302
+ await runMutationWithContext(async () => {
303
+ const call = await apiCall<{ ok: boolean; error?: string }>(
304
+ `/api/customer_accounts/admin/users/${encodeURIComponent(id)}/verify-email`,
305
+ { method: 'POST' },
306
+ )
307
+ if (!call.ok) {
308
+ flash(call.result?.error || t('customer_accounts.admin.detail.verifyEmail.error', 'Failed to verify email'), 'error')
309
+ return
310
+ }
311
+ flash(t('customer_accounts.admin.detail.verifyEmail.flash.success', 'Email marked as verified'), 'success')
312
+ setData((prev) => prev ? { ...prev, emailVerifiedAt: new Date().toISOString() } : prev)
313
+ }, { id })
314
+ } catch (err) {
315
+ const message = err instanceof Error ? err.message : t('customer_accounts.admin.detail.verifyEmail.error', 'Failed to verify email')
316
+ flash(message, 'error')
317
+ } finally {
318
+ setIsVerifying(false)
319
+ }
320
+ }, [confirm, data, id, runMutationWithContext, t])
321
+
322
+ const handleRevokeSession = React.useCallback(async (sessionId: string) => {
323
+ if (!id) return
324
+ try {
325
+ await runMutationWithContext(async () => {
326
+ const call = await apiCall(
327
+ `/api/customer_accounts/admin/users/${encodeURIComponent(id)}/sessions/${encodeURIComponent(sessionId)}`,
328
+ { method: 'DELETE' },
329
+ )
330
+ if (!call.ok) {
331
+ flash(t('customer_accounts.admin.detail.error.revokeSession', 'Failed to revoke session'), 'error')
332
+ return
333
+ }
334
+ flash(t('customer_accounts.admin.detail.flash.sessionRevoked', 'Session revoked'), 'success')
335
+ setData((prev) => {
336
+ if (!prev) return prev
337
+ return { ...prev, sessions: prev.sessions.filter((session) => session.id !== sessionId) }
338
+ })
339
+ }, { sessionId })
340
+ } catch (err) {
341
+ const message = err instanceof Error ? err.message : t('customer_accounts.admin.detail.error.revokeSession', 'Failed to revoke session')
342
+ flash(message, 'error')
343
+ }
344
+ }, [id, runMutationWithContext, t])
345
+
346
+ const handleRoleToggle = React.useCallback((roleId: string) => {
347
+ setSelectedRoleIds((prev) =>
348
+ prev.includes(roleId)
349
+ ? prev.filter((existingId) => existingId !== roleId)
350
+ : [...prev, roleId],
351
+ )
352
+ }, [])
353
+
354
+ if (isLoading) {
355
+ return (
356
+ <Page>
357
+ <PageBody>
358
+ <div className="flex h-[50vh] flex-col items-center justify-center gap-2 text-muted-foreground">
359
+ <Spinner className="h-6 w-6" />
360
+ <span>{t('customer_accounts.admin.detail.loading', 'Loading user...')}</span>
361
+ </div>
362
+ </PageBody>
363
+ </Page>
364
+ )
365
+ }
366
+
367
+ if (error || !data) {
368
+ return (
369
+ <Page>
370
+ <PageBody>
371
+ <div className="flex h-[50vh] flex-col items-center justify-center gap-2 text-muted-foreground">
372
+ <p>{error || t('customer_accounts.admin.detail.error.notFound', 'User not found')}</p>
373
+ <Button asChild variant="outline">
374
+ <Link href="/backend/customer_accounts">
375
+ {t('customer_accounts.admin.detail.actions.backToList', 'Back to list')}
376
+ </Link>
377
+ </Button>
378
+ </div>
379
+ </PageBody>
380
+ </Page>
381
+ )
382
+ }
383
+
384
+ return (
385
+ <Page>
386
+ <PageBody className="space-y-6">
387
+ <FormHeader
388
+ mode="detail"
389
+ backHref="/backend/customer_accounts"
390
+ backLabel={t('customer_accounts.admin.detail.actions.backToList', 'Back to list')}
391
+ title={data.displayName}
392
+ subtitle={data.email}
393
+ onDelete={() => { void handleDelete() }}
394
+ deleteLabel={t('customer_accounts.admin.detail.actions.delete', 'Delete')}
395
+ />
396
+
397
+ <div className="grid gap-6 md:grid-cols-2">
398
+ <div className="rounded-lg border p-4 space-y-3">
399
+ <h2 className="text-sm font-semibold">{t('customer_accounts.admin.detail.sections.info', 'User Information')}</h2>
400
+ <dl className="space-y-2 text-sm">
401
+ <div className="flex justify-between">
402
+ <dt className="text-muted-foreground">{t('customer_accounts.admin.detail.fields.email', 'Email')}</dt>
403
+ <dd>{data.email}</dd>
404
+ </div>
405
+ <div className="flex justify-between items-center">
406
+ <dt className="text-muted-foreground">{t('customer_accounts.admin.detail.fields.emailVerified', 'Email Verified')}</dt>
407
+ <dd className="flex items-center gap-2">
408
+ <span className={`inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium ${
409
+ data.emailVerifiedAt
410
+ ? 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200'
411
+ : 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200'
412
+ }`}>
413
+ {data.emailVerifiedAt
414
+ ? t('customer_accounts.admin.verified', 'Yes')
415
+ : t('customer_accounts.admin.unverified', 'No')}
416
+ </span>
417
+ {!data.emailVerifiedAt && (
418
+ <Button
419
+ variant="outline"
420
+ size="sm"
421
+ onClick={() => { void handleVerifyEmail() }}
422
+ disabled={isVerifying}
423
+ >
424
+ {isVerifying
425
+ ? t('customer_accounts.admin.detail.verifyEmail.actions.verifying', 'Verifying...')
426
+ : t('customer_accounts.admin.detail.verifyEmail.actions.verify', 'Mark Verified')}
427
+ </Button>
428
+ )}
429
+ </dd>
430
+ </div>
431
+ <div className="flex justify-between">
432
+ <dt className="text-muted-foreground">{t('customer_accounts.admin.detail.fields.lastLogin', 'Last Login')}</dt>
433
+ <dd>{formatDate(data.lastLoginAt, '-')}</dd>
434
+ </div>
435
+ <div className="flex justify-between">
436
+ <dt className="text-muted-foreground">{t('customer_accounts.admin.detail.fields.createdAt', 'Created')}</dt>
437
+ <dd>{formatDate(data.createdAt, '-')}</dd>
438
+ </div>
439
+ </dl>
440
+ </div>
441
+
442
+ <div className="rounded-lg border p-4 space-y-3">
443
+ <h2 className="text-sm font-semibold">{t('customer_accounts.admin.detail.sections.crmLinks', 'CRM Links')}</h2>
444
+ <dl className="space-y-2 text-sm">
445
+ <div className="flex justify-between">
446
+ <dt className="text-muted-foreground">{t('customer_accounts.admin.detail.fields.personEntity', 'Linked Person')}</dt>
447
+ <dd>
448
+ {data.personEntityId ? (
449
+ <Link href={`/backend/customers/people/${data.personEntityId}`} className="text-primary hover:underline">
450
+ {t('customer_accounts.admin.detail.actions.viewPerson', 'View')}
451
+ </Link>
452
+ ) : (
453
+ <span className="text-muted-foreground">-</span>
454
+ )}
455
+ </dd>
456
+ </div>
457
+ <div className="flex justify-between">
458
+ <dt className="text-muted-foreground">{t('customer_accounts.admin.detail.fields.customerEntity', 'Linked Company')}</dt>
459
+ <dd>
460
+ {data.customerEntityId ? (
461
+ <Link href={`/backend/customers/companies/${data.customerEntityId}`} className="text-primary hover:underline">
462
+ {t('customer_accounts.admin.detail.actions.viewCompany', 'View')}
463
+ </Link>
464
+ ) : (
465
+ <span className="text-muted-foreground">-</span>
466
+ )}
467
+ </dd>
468
+ </div>
469
+ </dl>
470
+ </div>
471
+ </div>
472
+
473
+ <div className="rounded-lg border p-4 space-y-4">
474
+ <h2 className="text-sm font-semibold">{t('customer_accounts.admin.detail.sections.settings', 'Account Settings')}</h2>
475
+ <div className="space-y-3">
476
+ <div className="space-y-2">
477
+ <label className="text-sm font-medium" htmlFor="user-display-name">
478
+ {t('customer_accounts.admin.detail.fields.displayName', 'Display Name')}
479
+ </label>
480
+ <input
481
+ id="user-display-name"
482
+ type="text"
483
+ value={editDisplayName}
484
+ onChange={(event) => setEditDisplayName(event.target.value)}
485
+ className="flex h-9 w-full max-w-sm rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
486
+ />
487
+ </div>
488
+
489
+ <div className="flex items-center gap-3">
490
+ <label className="text-sm font-medium" htmlFor="user-active-toggle">
491
+ {t('customer_accounts.admin.detail.fields.isActive', 'Active')}
492
+ </label>
493
+ <button
494
+ id="user-active-toggle"
495
+ type="button"
496
+ role="switch"
497
+ aria-checked={editActive ?? data.isActive}
498
+ onClick={() => setEditActive((prev) => !(prev ?? data.isActive))}
499
+ className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${
500
+ (editActive ?? data.isActive) ? 'bg-primary' : 'bg-muted'
501
+ }`}
502
+ >
503
+ <span className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
504
+ (editActive ?? data.isActive) ? 'translate-x-6' : 'translate-x-1'
505
+ }`} />
506
+ </button>
507
+ </div>
508
+
509
+ <div className="space-y-2">
510
+ <p className="text-sm font-medium">{t('customer_accounts.admin.detail.fields.roles', 'Roles')}</p>
511
+ <div className="flex flex-wrap gap-2">
512
+ {availableRoles.map((role) => {
513
+ const isSelected = selectedRoleIds.includes(role.id)
514
+ return (
515
+ <button
516
+ key={role.id}
517
+ type="button"
518
+ onClick={() => handleRoleToggle(role.id)}
519
+ className={`rounded-full border px-3 py-1 text-xs font-medium transition-colors ${
520
+ isSelected
521
+ ? 'border-primary bg-primary/10 text-primary'
522
+ : 'border-border bg-background text-muted-foreground hover:bg-muted'
523
+ }`}
524
+ >
525
+ {role.name}
526
+ </button>
527
+ )
528
+ })}
529
+ {availableRoles.length === 0 && (
530
+ <span className="text-sm text-muted-foreground">
531
+ {t('customer_accounts.admin.detail.noRolesAvailable', 'No roles available')}
532
+ </span>
533
+ )}
534
+ </div>
535
+ </div>
536
+
537
+ <div className="pt-2">
538
+ <Button onClick={() => { void handleSave() }} disabled={isSaving}>
539
+ {isSaving
540
+ ? t('customer_accounts.admin.detail.actions.saving', 'Saving...')
541
+ : t('customer_accounts.admin.detail.actions.save', 'Save Changes')}
542
+ </Button>
543
+ </div>
544
+ </div>
545
+ </div>
546
+
547
+ <div className="rounded-lg border p-4 space-y-3">
548
+ <div className="flex items-center justify-between">
549
+ <h2 className="text-sm font-semibold">{t('customer_accounts.admin.detail.sections.security', 'Security')}</h2>
550
+ </div>
551
+ <div className="flex gap-2">
552
+ <Button variant="outline" onClick={() => setResetPasswordOpen(true)}>
553
+ {t('customer_accounts.admin.detail.resetPassword.actions.open', 'Reset Password')}
554
+ </Button>
555
+ </div>
556
+ </div>
557
+
558
+ <div className="rounded-lg border p-4 space-y-3">
559
+ <h2 className="text-sm font-semibold">
560
+ {t('customer_accounts.admin.detail.sections.sessions', 'Active Sessions')}
561
+ {data.sessions.length > 0 && (
562
+ <span className="ml-2 text-xs font-normal text-muted-foreground">({data.sessions.length})</span>
563
+ )}
564
+ </h2>
565
+ {data.sessions.length === 0 ? (
566
+ <p className="text-sm text-muted-foreground">
567
+ {t('customer_accounts.admin.detail.noSessions', 'No active sessions')}
568
+ </p>
569
+ ) : (
570
+ <div className="divide-y">
571
+ {data.sessions.map((session) => (
572
+ <div key={session.id} className="flex items-center justify-between py-2 text-sm">
573
+ <div className="space-y-0.5">
574
+ <p className="font-medium">
575
+ {session.ipAddress || t('customer_accounts.admin.detail.unknownIp', 'Unknown IP')}
576
+ </p>
577
+ <p className="text-xs text-muted-foreground truncate max-w-xs">
578
+ {session.userAgent || t('customer_accounts.admin.detail.unknownDevice', 'Unknown device')}
579
+ </p>
580
+ <p className="text-xs text-muted-foreground">
581
+ {t('customer_accounts.admin.detail.fields.lastUsed', 'Last used')}: {formatDate(session.lastUsedAt, '-')}
582
+ </p>
583
+ </div>
584
+ <Button
585
+ variant="outline"
586
+ size="sm"
587
+ onClick={() => { void handleRevokeSession(session.id) }}
588
+ >
589
+ {t('customer_accounts.admin.detail.actions.revoke', 'Revoke')}
590
+ </Button>
591
+ </div>
592
+ ))}
593
+ </div>
594
+ )}
595
+ </div>
596
+
597
+ <ResetPasswordDialog
598
+ open={resetPasswordOpen}
599
+ onOpenChange={setResetPasswordOpen}
600
+ userId={id!}
601
+ onRunMutation={runMutationWithContext}
602
+ />
603
+ </PageBody>
604
+ {ConfirmDialogElement}
605
+ </Page>
606
+ )
607
+ }
@@ -0,0 +1,12 @@
1
+ export const metadata = {
2
+ requireAuth: true,
3
+ requireFeatures: ['customer_accounts.roles.manage'],
4
+ pageTitle: 'Customer Role Detail',
5
+ pageTitleKey: 'customer_accounts.nav.role_detail',
6
+ navHidden: true,
7
+ breadcrumb: [
8
+ { label: 'Customer Accounts', labelKey: 'customer_accounts.nav.users', href: '/backend/customer_accounts' },
9
+ { label: 'Roles', labelKey: 'customer_accounts.nav.roles', href: '/backend/customer_accounts/roles' },
10
+ { label: 'Role Detail', labelKey: 'customer_accounts.nav.role_detail' },
11
+ ],
12
+ } as const