@open-mercato/core 0.5.1-develop.2638.59e6e26f46 → 0.5.1-develop.2657.a01847a9fa

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 (58) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/AGENTS.md +26 -0
  3. package/dist/modules/auth/lib/backendChrome.js +3 -1
  4. package/dist/modules/auth/lib/backendChrome.js.map +2 -2
  5. package/dist/modules/auth/services/rbacService.js +8 -2
  6. package/dist/modules/auth/services/rbacService.js.map +2 -2
  7. package/dist/modules/customer_accounts/api/password/reset-confirm.js +7 -0
  8. package/dist/modules/customer_accounts/api/password/reset-confirm.js.map +2 -2
  9. package/dist/modules/customer_accounts/api/portal/nav.js +77 -0
  10. package/dist/modules/customer_accounts/api/portal/nav.js.map +7 -0
  11. package/dist/modules/customer_accounts/api/signup.js +20 -8
  12. package/dist/modules/customer_accounts/api/signup.js.map +2 -2
  13. package/dist/modules/customer_accounts/services/customerSessionService.js +32 -0
  14. package/dist/modules/customer_accounts/services/customerSessionService.js.map +2 -2
  15. package/dist/modules/directory/api/organizations/route.js +10 -0
  16. package/dist/modules/directory/api/organizations/route.js.map +3 -3
  17. package/dist/modules/directory/backend/directory/organizations/[id]/edit/page.js +13 -2
  18. package/dist/modules/directory/backend/directory/organizations/[id]/edit/page.js.map +2 -2
  19. package/dist/modules/directory/backend/directory/organizations/create/page.js +12 -2
  20. package/dist/modules/directory/backend/directory/organizations/create/page.js.map +2 -2
  21. package/dist/modules/messages/components/message-detail/hooks/useMessageDetails.js +4 -3
  22. package/dist/modules/messages/components/message-detail/hooks/useMessageDetails.js.map +2 -2
  23. package/dist/modules/portal/frontend/[orgSlug]/portal/dashboard/page.meta.js +17 -0
  24. package/dist/modules/portal/frontend/[orgSlug]/portal/dashboard/page.meta.js.map +7 -0
  25. package/dist/modules/portal/frontend/[orgSlug]/portal/login/page.meta.js +11 -0
  26. package/dist/modules/portal/frontend/[orgSlug]/portal/login/page.meta.js.map +7 -0
  27. package/dist/modules/portal/frontend/[orgSlug]/portal/page.meta.js +11 -0
  28. package/dist/modules/portal/frontend/[orgSlug]/portal/page.meta.js.map +7 -0
  29. package/dist/modules/portal/frontend/[orgSlug]/portal/profile/page.meta.js +17 -0
  30. package/dist/modules/portal/frontend/[orgSlug]/portal/profile/page.meta.js.map +7 -0
  31. package/dist/modules/portal/frontend/[orgSlug]/portal/signup/page.meta.js +11 -0
  32. package/dist/modules/portal/frontend/[orgSlug]/portal/signup/page.meta.js.map +7 -0
  33. package/dist/modules/portal/frontend/[orgSlug]/portal/verify/page.meta.js +11 -0
  34. package/dist/modules/portal/frontend/[orgSlug]/portal/verify/page.meta.js.map +7 -0
  35. package/dist/modules/workflows/lib/activity-executor.js +25 -16
  36. package/dist/modules/workflows/lib/activity-executor.js.map +2 -2
  37. package/package.json +3 -3
  38. package/src/modules/auth/lib/backendChrome.tsx +3 -1
  39. package/src/modules/auth/services/rbacService.ts +8 -2
  40. package/src/modules/customer_accounts/api/password/reset-confirm.ts +9 -0
  41. package/src/modules/customer_accounts/api/portal/nav.ts +87 -0
  42. package/src/modules/customer_accounts/api/signup.ts +23 -7
  43. package/src/modules/customer_accounts/services/customerSessionService.ts +39 -0
  44. package/src/modules/directory/api/organizations/route.ts +11 -0
  45. package/src/modules/directory/backend/directory/organizations/[id]/edit/page.tsx +17 -3
  46. package/src/modules/directory/backend/directory/organizations/create/page.tsx +15 -3
  47. package/src/modules/directory/i18n/de.json +2 -0
  48. package/src/modules/directory/i18n/en.json +2 -0
  49. package/src/modules/directory/i18n/es.json +2 -0
  50. package/src/modules/directory/i18n/pl.json +2 -0
  51. package/src/modules/messages/components/message-detail/hooks/useMessageDetails.ts +4 -3
  52. package/src/modules/portal/frontend/[orgSlug]/portal/dashboard/page.meta.ts +15 -0
  53. package/src/modules/portal/frontend/[orgSlug]/portal/login/page.meta.ts +9 -0
  54. package/src/modules/portal/frontend/[orgSlug]/portal/page.meta.ts +9 -0
  55. package/src/modules/portal/frontend/[orgSlug]/portal/profile/page.meta.ts +15 -0
  56. package/src/modules/portal/frontend/[orgSlug]/portal/signup/page.meta.ts +9 -0
  57. package/src/modules/portal/frontend/[orgSlug]/portal/verify/page.meta.ts +9 -0
  58. package/src/modules/workflows/lib/activity-executor.ts +52 -24
@@ -24,6 +24,7 @@ type OrganizationResponse = {
24
24
  items: Array<{
25
25
  id: string
26
26
  name: string
27
+ slug?: string | null
27
28
  tenantId: string
28
29
  tenantName?: string | null
29
30
  parentId: string | null
@@ -131,6 +132,7 @@ export default function EditOrganizationPage({ params }: { params?: { id?: strin
131
132
  setInitialValues({
132
133
  id: record.id,
133
134
  name: record.name,
135
+ slug: record.slug ?? '',
134
136
  parentId: record.parentId || '',
135
137
  isActive: record.isActive,
136
138
  tenantId: resolvedTenantId,
@@ -194,6 +196,12 @@ export default function EditOrganizationPage({ params }: { params?: { id?: strin
194
196
  } as CrudField,
195
197
  ] : []),
196
198
  { id: 'name', label: t('directory.organizations.form.field.name', 'Name'), type: 'text', required: true },
199
+ {
200
+ id: 'slug',
201
+ label: t('directory.organizations.form.field.slug', 'Slug'),
202
+ type: 'text',
203
+ description: t('directory.organizations.form.field.slug.description', 'URL-safe identifier used for the customer portal (lowercase letters, numbers, hyphens, underscores).'),
204
+ },
197
205
  {
198
206
  id: 'parentId',
199
207
  label: t('directory.organizations.form.field.parent', 'Parent'),
@@ -237,8 +245,8 @@ export default function EditOrganizationPage({ params }: { params?: { id?: strin
237
245
 
238
246
  const detailFields = React.useMemo(() => (
239
247
  actorIsSuperAdmin
240
- ? ['tenantId', 'name', 'parentId', 'childrenInfo', 'isActive']
241
- : ['name', 'parentId', 'childrenInfo', 'isActive']
248
+ ? ['tenantId', 'name', 'slug', 'parentId', 'childrenInfo', 'isActive']
249
+ : ['name', 'slug', 'parentId', 'childrenInfo', 'isActive']
242
250
  ), [actorIsSuperAdmin])
243
251
 
244
252
  const groups: CrudFormGroup[] = React.useMemo(() => ([
@@ -278,7 +286,7 @@ export default function EditOrganizationPage({ params }: { params?: { id?: strin
278
286
  fields={fields}
279
287
  groups={groups}
280
288
  entityId={E.directory.organization}
281
- initialValues={initialValues ?? { id: orgId, tenantId: tenantId ?? null, name: '', parentId: '', isActive: true, childIds: [] }}
289
+ initialValues={initialValues ?? { id: orgId, tenantId: tenantId ?? null, name: '', slug: '', parentId: '', isActive: true, childIds: [] }}
282
290
  isLoading={loading}
283
291
  loadingMessage={t('directory.organizations.form.loading', 'Loading organization...')}
284
292
  submitLabel={t('directory.organizations.form.action.save', 'Save')}
@@ -315,6 +323,7 @@ export default function EditOrganizationPage({ params }: { params?: { id?: strin
315
323
  type UpdateOrganizationPayload = {
316
324
  id: string
317
325
  name: string
326
+ slug?: string | null
318
327
  isActive: boolean
319
328
  parentId: string | null
320
329
  childIds: string[]
@@ -371,6 +380,11 @@ export async function submitUpdateOrganization(options: {
371
380
  childIds: originalChildIds,
372
381
  }
373
382
 
383
+ if (typeof values.slug === 'string') {
384
+ const trimmedSlug = values.slug.trim()
385
+ payload.slug = trimmedSlug.length ? trimmedSlug : null
386
+ }
387
+
374
388
  if (submittedTenantId !== undefined && submittedTenantId !== null) {
375
389
  payload.tenantId = submittedTenantId
376
390
  }
@@ -132,6 +132,12 @@ export default function CreateOrganizationPage() {
132
132
  } as CrudField,
133
133
  ] : []),
134
134
  { id: 'name', label: t('directory.organizations.form.field.name', 'Name'), type: 'text', required: true },
135
+ {
136
+ id: 'slug',
137
+ label: t('directory.organizations.form.field.slug', 'Slug'),
138
+ type: 'text',
139
+ description: t('directory.organizations.form.field.slug.description', 'URL-safe identifier used for the customer portal (lowercase letters, numbers, hyphens, underscores). Generated from the name when left blank.'),
140
+ },
135
141
  {
136
142
  id: 'parentId',
137
143
  label: t('directory.organizations.form.field.parent', 'Parent'),
@@ -166,8 +172,8 @@ export default function CreateOrganizationPage() {
166
172
 
167
173
  const detailFields = React.useMemo(() => (
168
174
  actorIsSuperAdmin
169
- ? ['tenantId', 'name', 'parentId', 'childIds', 'isActive']
170
- : ['name', 'parentId', 'childIds', 'isActive']
175
+ ? ['tenantId', 'name', 'slug', 'parentId', 'childIds', 'isActive']
176
+ : ['name', 'slug', 'parentId', 'childIds', 'isActive']
171
177
  ), [actorIsSuperAdmin])
172
178
 
173
179
  const groups: CrudFormGroup[] = React.useMemo(() => ([
@@ -186,7 +192,7 @@ export default function CreateOrganizationPage() {
186
192
  fields={fields}
187
193
  groups={groups}
188
194
  entityId={E.directory.organization}
189
- initialValues={{ tenantId: selectedTenantId ?? null, name: '', parentId: '', childIds: [], isActive: true }}
195
+ initialValues={{ tenantId: selectedTenantId ?? null, name: '', slug: '', parentId: '', childIds: [], isActive: true }}
190
196
  submitLabel={t('directory.organizations.form.action.create', 'Create')}
191
197
  cancelHref="/backend/directory/organizations"
192
198
  successRedirect={`/backend/directory/organizations?flash=${successMessage}&type=success`}
@@ -208,6 +214,7 @@ export default function CreateOrganizationPage() {
208
214
 
209
215
  type CreateOrganizationPayload = {
210
216
  name: string
217
+ slug?: string | null
211
218
  isActive: boolean
212
219
  parentId: string | null
213
220
  childIds: string[]
@@ -261,6 +268,11 @@ export async function submitCreateOrganization(options: {
261
268
  childIds: Array.isArray(values.childIds) ? values.childIds.filter((id): id is string => typeof id === 'string') : [],
262
269
  }
263
270
 
271
+ if (typeof values.slug === 'string') {
272
+ const trimmedSlug = values.slug.trim()
273
+ if (trimmedSlug.length) payload.slug = trimmedSlug
274
+ }
275
+
264
276
  if (tenantValue) payload.tenantId = tenantValue
265
277
  if (Object.keys(customFields).length > 0) payload.customFields = customFields
266
278
 
@@ -27,6 +27,8 @@
27
27
  "directory.organizations.form.field.isActive": "Aktiv",
28
28
  "directory.organizations.form.field.name": "Name",
29
29
  "directory.organizations.form.field.parent": "Übergeordnete Organisation",
30
+ "directory.organizations.form.field.slug": "Slug",
31
+ "directory.organizations.form.field.slug.description": "URL-sicherer Bezeichner für das Kundenportal (Kleinbuchstaben, Zahlen, Bindestriche, Unterstriche).",
30
32
  "directory.organizations.form.field.tenant": "Mandant",
31
33
  "directory.organizations.form.group.customFields": "Benutzerdefinierte Daten",
32
34
  "directory.organizations.form.group.details": "Details",
@@ -27,6 +27,8 @@
27
27
  "directory.organizations.form.field.isActive": "Active",
28
28
  "directory.organizations.form.field.name": "Name",
29
29
  "directory.organizations.form.field.parent": "Parent",
30
+ "directory.organizations.form.field.slug": "Slug",
31
+ "directory.organizations.form.field.slug.description": "URL-safe identifier used for the customer portal (lowercase letters, numbers, hyphens, underscores).",
30
32
  "directory.organizations.form.field.tenant": "Tenant",
31
33
  "directory.organizations.form.group.customFields": "Custom Data",
32
34
  "directory.organizations.form.group.details": "Details",
@@ -27,6 +27,8 @@
27
27
  "directory.organizations.form.field.isActive": "Activo",
28
28
  "directory.organizations.form.field.name": "Nombre",
29
29
  "directory.organizations.form.field.parent": "Padre",
30
+ "directory.organizations.form.field.slug": "Slug",
31
+ "directory.organizations.form.field.slug.description": "Identificador seguro para URL usado en el portal de clientes (minúsculas, números, guiones, guiones bajos).",
30
32
  "directory.organizations.form.field.tenant": "Inquilino",
31
33
  "directory.organizations.form.group.customFields": "Datos personalizados",
32
34
  "directory.organizations.form.group.details": "Detalles",
@@ -27,6 +27,8 @@
27
27
  "directory.organizations.form.field.isActive": "Aktywna",
28
28
  "directory.organizations.form.field.name": "Nazwa",
29
29
  "directory.organizations.form.field.parent": "Organizacja nadrzędna",
30
+ "directory.organizations.form.field.slug": "Identyfikator URL",
31
+ "directory.organizations.form.field.slug.description": "Identyfikator używany w adresie portalu klienta (małe litery, cyfry, myślniki, podkreślenia).",
30
32
  "directory.organizations.form.field.tenant": "Najemca",
31
33
  "directory.organizations.form.group.customFields": "Dane niestandardowe",
32
34
  "directory.organizations.form.group.details": "Szczegóły",
@@ -23,8 +23,9 @@ export function useMessageDetails(id: string) {
23
23
  queryClient,
24
24
  })
25
25
 
26
- const invalidateDetailQueries = React.useCallback(
26
+ const invalidateMessageQueries = React.useCallback(
27
27
  (payload: Record<string, unknown>) => {
28
+ void queryClient.invalidateQueries({ queryKey: ['messages', 'list'] })
28
29
  void queryClient.invalidateQueries({ queryKey: ['messages', 'detail', id] })
29
30
  const messageId = typeof payload.messageId === 'string' ? payload.messageId : null
30
31
  if (messageId && messageId !== id) {
@@ -37,9 +38,9 @@ export function useMessageDetails(id: string) {
37
38
  useAppEvent(
38
39
  'messages.message.*',
39
40
  (evt) => {
40
- invalidateDetailQueries((evt.payload ?? {}) as Record<string, unknown>)
41
+ invalidateMessageQueries((evt.payload ?? {}) as Record<string, unknown>)
41
42
  },
42
- [invalidateDetailQueries],
43
+ [invalidateMessageQueries],
43
44
  )
44
45
 
45
46
  useAppEvent(
@@ -0,0 +1,15 @@
1
+ import type { PageMetadata } from '@open-mercato/shared/modules/registry'
2
+
3
+ export const metadata: PageMetadata = {
4
+ requireCustomerAuth: true,
5
+ titleKey: 'portal.dashboard.title',
6
+ title: 'Dashboard',
7
+ nav: {
8
+ label: 'Dashboard',
9
+ labelKey: 'portal.nav.dashboard',
10
+ group: 'main',
11
+ order: 10,
12
+ },
13
+ }
14
+
15
+ export default metadata
@@ -0,0 +1,9 @@
1
+ import type { PageMetadata } from '@open-mercato/shared/modules/registry'
2
+
3
+ export const metadata: PageMetadata = {
4
+ titleKey: 'portal.nav.login',
5
+ title: 'Log In',
6
+ navHidden: true,
7
+ }
8
+
9
+ export default metadata
@@ -0,0 +1,9 @@
1
+ import type { PageMetadata } from '@open-mercato/shared/modules/registry'
2
+
3
+ export const metadata: PageMetadata = {
4
+ titleKey: 'portal.title',
5
+ title: 'Customer Portal',
6
+ navHidden: true,
7
+ }
8
+
9
+ export default metadata
@@ -0,0 +1,15 @@
1
+ import type { PageMetadata } from '@open-mercato/shared/modules/registry'
2
+
3
+ export const metadata: PageMetadata = {
4
+ requireCustomerAuth: true,
5
+ titleKey: 'portal.nav.profile',
6
+ title: 'Profile',
7
+ nav: {
8
+ label: 'Profile',
9
+ labelKey: 'portal.nav.profile',
10
+ group: 'account',
11
+ order: 10,
12
+ },
13
+ }
14
+
15
+ export default metadata
@@ -0,0 +1,9 @@
1
+ import type { PageMetadata } from '@open-mercato/shared/modules/registry'
2
+
3
+ export const metadata: PageMetadata = {
4
+ titleKey: 'portal.nav.signup',
5
+ title: 'Sign Up',
6
+ navHidden: true,
7
+ }
8
+
9
+ export default metadata
@@ -0,0 +1,9 @@
1
+ import type { PageMetadata } from '@open-mercato/shared/modules/registry'
2
+
3
+ export const metadata: PageMetadata = {
4
+ titleKey: 'portal.title',
5
+ title: 'Verify Email',
6
+ navHidden: true,
7
+ }
8
+
9
+ export default metadata
@@ -788,10 +788,13 @@ export async function executeCallApi(
788
788
  // SECURITY.md changelog entry for this fix.
789
789
  //
790
790
  // The resolution strategy is:
791
- // 1. Use the workflow instance's `createdBy` user (whoever manually
792
- // started the instance), when available.
793
- // 2. Fall back to the workflow definition's `createdBy` (author) when
794
- // the instance was started by an event trigger with no user.
791
+ // 1. Use the workflow instance's `metadata.initiatedBy` user (whoever
792
+ // manually started the instance), when available. Only this user's
793
+ // current active roles are used — we never fall back to the author
794
+ // when the initiator is known, because that would escalate the
795
+ // initiator's privileges.
796
+ // 2. Fall back to the workflow definition's `createdBy` (author) only
797
+ // when the instance was started by an event trigger with no user.
795
798
  // 3. If no traceable principal exists, the activity refuses to run —
796
799
  // there is no "system" fallback that bypasses RBAC.
797
800
  const resolvedRoleIds = await resolveCallApiRoleIds(apiKeyEm, context.workflowInstance)
@@ -885,38 +888,28 @@ export type CallApiInstanceLike = {
885
888
  tenantId: string
886
889
  organizationId: string
887
890
  definitionId: string
891
+ metadata?: { initiatedBy?: string | null } | null
888
892
  }
889
893
 
890
- export async function resolveCallApiRoleIds(
894
+ async function resolveActiveRoleIdsForUser(
891
895
  em: any,
892
- instance: CallApiInstanceLike
896
+ userId: string,
897
+ scope: { tenantId: string; organizationId: string },
893
898
  ): Promise<string[]> {
894
- if (!instance.definitionId) return []
895
-
896
899
  const { findOneWithDecryption, findWithDecryption } = await import('@open-mercato/shared/lib/encryption/find')
897
900
  const { User, UserRole, Role } = await import('../../auth/data/entities')
898
- const { WorkflowDefinition } = await import('../data/entities')
899
901
 
900
- const scope = { tenantId: instance.tenantId, organizationId: instance.organizationId }
901
-
902
- const definition = await findOneWithDecryption(em, WorkflowDefinition, {
903
- id: instance.definitionId,
904
- tenantId: instance.tenantId,
905
- }, {}, scope)
906
- const authorUserId = definition?.createdBy
907
- if (!authorUserId) return []
908
-
909
- const author = await findOneWithDecryption(em, User, {
910
- id: authorUserId,
911
- tenantId: instance.tenantId,
902
+ const user = await findOneWithDecryption(em, User, {
903
+ id: userId,
904
+ tenantId: scope.tenantId,
912
905
  deletedAt: null,
913
906
  }, {}, scope)
914
- if (!author) return []
907
+ if (!user) return []
915
908
 
916
909
  const userRoles = await findWithDecryption(
917
910
  em,
918
911
  UserRole,
919
- { user: author.id, deletedAt: null },
912
+ { user: user.id, deletedAt: null },
920
913
  { populate: ['role'] },
921
914
  scope,
922
915
  )
@@ -928,12 +921,47 @@ export async function resolveCallApiRoleIds(
928
921
 
929
922
  const scopedRoles = await findWithDecryption(em, Role, {
930
923
  id: { $in: roleIds },
931
- tenantId: instance.tenantId,
924
+ tenantId: scope.tenantId,
932
925
  deletedAt: null,
933
926
  }, {}, scope)
934
927
  return scopedRoles.map((r: any) => r.id as string)
935
928
  }
936
929
 
930
+ export async function resolveCallApiRoleIds(
931
+ em: any,
932
+ instance: CallApiInstanceLike
933
+ ): Promise<string[]> {
934
+ if (!instance.definitionId) return []
935
+
936
+ const { findOneWithDecryption } = await import('@open-mercato/shared/lib/encryption/find')
937
+ const { WorkflowDefinition } = await import('../data/entities')
938
+
939
+ const scope = { tenantId: instance.tenantId, organizationId: instance.organizationId }
940
+
941
+ // 1. Prefer the triggering user (whoever manually started this instance).
942
+ // WorkflowInstance.metadata.initiatedBy is the canonical record of that
943
+ // principal for user-started instances; use their current role set so
944
+ // CALL_API never exceeds the initiator's permissions. Refuse if the
945
+ // initiator has no active scoped roles — do not fall back to the
946
+ // definition author, which would escalate the initiator's privileges.
947
+ const initiatorUserId = instance.metadata?.initiatedBy ?? null
948
+ if (initiatorUserId) {
949
+ return resolveActiveRoleIdsForUser(em, initiatorUserId, scope)
950
+ }
951
+
952
+ // 2. Event-triggered instance with no human initiator: fall back to the
953
+ // definition author. Soft-deleted definitions must not mint keys.
954
+ const definition = await findOneWithDecryption(em, WorkflowDefinition, {
955
+ id: instance.definitionId,
956
+ tenantId: instance.tenantId,
957
+ deletedAt: null,
958
+ }, {}, scope)
959
+ const authorUserId = definition?.createdBy
960
+ if (!authorUserId) return []
961
+
962
+ return resolveActiveRoleIdsForUser(em, authorUserId, scope)
963
+ }
964
+
937
965
  /**
938
966
  * Build full API URL from endpoint
939
967
  * - Relative paths (/api/...) → prepend APP_URL