@open-mercato/core 0.6.8-develop.6918.1.7b3f32b0c8 → 0.6.8-develop.6925.1.cefa200fa4

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 (95) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/bootstrap.js +2 -1
  3. package/dist/bootstrap.js.map +2 -2
  4. package/dist/generated/entities/role/index.js +2 -0
  5. package/dist/generated/entities/role/index.js.map +2 -2
  6. package/dist/generated/entity-fields-registry.js +1 -0
  7. package/dist/generated/entity-fields-registry.js.map +2 -2
  8. package/dist/helpers/integration/api.js +4 -0
  9. package/dist/helpers/integration/api.js.map +2 -2
  10. package/dist/modules/audit_logs/api/audit-logs/actions/redo/route.js +5 -0
  11. package/dist/modules/audit_logs/api/audit-logs/actions/redo/route.js.map +2 -2
  12. package/dist/modules/audit_logs/api/audit-logs/actions/undo/route.js +5 -0
  13. package/dist/modules/audit_logs/api/audit-logs/actions/undo/route.js.map +2 -2
  14. package/dist/modules/audit_logs/backend/audit-logs/page.js +4 -4
  15. package/dist/modules/audit_logs/backend/audit-logs/page.js.map +2 -2
  16. package/dist/modules/audit_logs/components/AuditLogsActions.js +9 -2
  17. package/dist/modules/audit_logs/components/AuditLogsActions.js.map +2 -2
  18. package/dist/modules/auth/api/login.js +5 -2
  19. package/dist/modules/auth/api/login.js.map +2 -2
  20. package/dist/modules/auth/api/users/route.js +8 -5
  21. package/dist/modules/auth/api/users/route.js.map +2 -2
  22. package/dist/modules/auth/backend/users/[id]/edit/page.js +15 -1
  23. package/dist/modules/auth/backend/users/[id]/edit/page.js.map +2 -2
  24. package/dist/modules/auth/backend/users/page.js +7 -1
  25. package/dist/modules/auth/backend/users/page.js.map +2 -2
  26. package/dist/modules/auth/commands/users.js +175 -76
  27. package/dist/modules/auth/commands/users.js.map +2 -2
  28. package/dist/modules/auth/data/entities.js +4 -0
  29. package/dist/modules/auth/data/entities.js.map +2 -2
  30. package/dist/modules/auth/lib/sessionIntegrity.js +1 -1
  31. package/dist/modules/auth/lib/sessionIntegrity.js.map +2 -2
  32. package/dist/modules/auth/lib/setup-app.js +1 -1
  33. package/dist/modules/auth/lib/setup-app.js.map +2 -2
  34. package/dist/modules/auth/migrations/Migration20260728134212_auth.js +14 -0
  35. package/dist/modules/auth/migrations/Migration20260728134212_auth.js.map +7 -0
  36. package/dist/modules/catalog/search.js +8 -0
  37. package/dist/modules/catalog/search.js.map +2 -2
  38. package/dist/modules/customer_accounts/search.js +2 -0
  39. package/dist/modules/customer_accounts/search.js.map +2 -2
  40. package/dist/modules/customers/commands/deals.js +55 -8
  41. package/dist/modules/customers/commands/deals.js.map +2 -2
  42. package/dist/modules/customers/components/detail/create/dealCustomFieldControl.js +1 -1
  43. package/dist/modules/customers/components/detail/create/dealCustomFieldControl.js.map +2 -2
  44. package/dist/modules/customers/components/formConfig.js +2 -0
  45. package/dist/modules/customers/components/formConfig.js.map +2 -2
  46. package/dist/modules/inbox_ops/search.js +1 -0
  47. package/dist/modules/inbox_ops/search.js.map +2 -2
  48. package/dist/modules/messages/search.js +2 -1
  49. package/dist/modules/messages/search.js.map +2 -2
  50. package/dist/modules/planner/search.js +1 -0
  51. package/dist/modules/planner/search.js.map +2 -2
  52. package/dist/modules/resources/search.js +2 -0
  53. package/dist/modules/resources/search.js.map +2 -2
  54. package/dist/modules/sales/search.js +24 -2
  55. package/dist/modules/sales/search.js.map +2 -2
  56. package/dist/modules/staff/search.js +4 -0
  57. package/dist/modules/staff/search.js.map +2 -2
  58. package/dist/modules/wms/search.js +4 -0
  59. package/dist/modules/wms/search.js.map +2 -2
  60. package/generated/entities/role/index.ts +1 -0
  61. package/generated/entity-fields-registry.ts +1 -0
  62. package/package.json +7 -7
  63. package/src/bootstrap.ts +9 -2
  64. package/src/helpers/integration/api.ts +13 -0
  65. package/src/modules/audit_logs/api/audit-logs/actions/redo/route.ts +7 -0
  66. package/src/modules/audit_logs/api/audit-logs/actions/undo/route.ts +9 -0
  67. package/src/modules/audit_logs/backend/audit-logs/page.tsx +4 -4
  68. package/src/modules/audit_logs/components/AuditLogsActions.tsx +20 -4
  69. package/src/modules/auth/api/login.ts +9 -2
  70. package/src/modules/auth/api/users/route.ts +8 -4
  71. package/src/modules/auth/backend/users/[id]/edit/page.tsx +17 -1
  72. package/src/modules/auth/backend/users/page.tsx +11 -0
  73. package/src/modules/auth/commands/users.ts +251 -85
  74. package/src/modules/auth/data/entities.ts +3 -0
  75. package/src/modules/auth/i18n/de.json +6 -0
  76. package/src/modules/auth/i18n/en.json +6 -0
  77. package/src/modules/auth/i18n/es.json +6 -0
  78. package/src/modules/auth/i18n/ko.json +6 -0
  79. package/src/modules/auth/i18n/pl.json +6 -0
  80. package/src/modules/auth/lib/sessionIntegrity.ts +1 -1
  81. package/src/modules/auth/lib/setup-app.ts +1 -1
  82. package/src/modules/auth/migrations/.snapshot-open-mercato.json +16 -0
  83. package/src/modules/auth/migrations/Migration20260728134212_auth.ts +14 -0
  84. package/src/modules/catalog/search.ts +8 -0
  85. package/src/modules/customer_accounts/search.ts +2 -0
  86. package/src/modules/customers/commands/deals.ts +86 -14
  87. package/src/modules/customers/components/detail/create/dealCustomFieldControl.tsx +1 -1
  88. package/src/modules/customers/components/formConfig.tsx +2 -0
  89. package/src/modules/inbox_ops/search.ts +1 -0
  90. package/src/modules/messages/search.ts +2 -1
  91. package/src/modules/planner/search.ts +1 -0
  92. package/src/modules/resources/search.ts +2 -0
  93. package/src/modules/sales/search.ts +24 -2
  94. package/src/modules/staff/search.ts +4 -0
  95. package/src/modules/wms/search.ts +4 -0
@@ -9,7 +9,7 @@ import {
9
9
  } from "@open-mercato/shared/lib/commands/helpers";
10
10
  import { CrudHttpError } from "@open-mercato/shared/lib/crud/errors";
11
11
  import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
12
- import { UniqueConstraintViolationException } from "@mikro-orm/core";
12
+ import { UniqueConstraintViolationException, LockMode } from "@mikro-orm/core";
13
13
  import { User, UserRole, Role, UserAcl, Session, PasswordReset } from "@open-mercato/core/modules/auth/data/entities";
14
14
  import { Organization } from "@open-mercato/core/modules/directory/data/entities";
15
15
  import { E } from "../../../generated/entities.ids.generated.js";
@@ -74,7 +74,8 @@ const updateSchema = z.object({
74
74
  name: displayNameSchema,
75
75
  password: passwordSchema.optional(),
76
76
  organizationId: z.string().uuid().optional(),
77
- roles: z.array(z.string()).optional()
77
+ roles: z.array(z.string()).optional(),
78
+ isConfirmed: z.boolean().optional()
78
79
  });
79
80
  const userCrudEvents = {
80
81
  module: "auth",
@@ -256,6 +257,9 @@ const createUserCommand = {
256
257
  let removed = null;
257
258
  await withAtomicFlush(em, [
258
259
  async () => {
260
+ const current = await findOneWithDecryption(em, User, { id: userId, deletedAt: null }, {}, { tenantId: null, organizationId: null });
261
+ const floorTenantId = current?.tenantId ? String(current.tenantId) : snapshot?.tenantId ?? null;
262
+ await enforceProtectedRoleFloor(em, floorTenantId, userId, { deleting: true }, ctx);
259
263
  await em.nativeDelete(UserAcl, { user: userId });
260
264
  await em.nativeDelete(UserRole, { user: userId });
261
265
  await em.nativeDelete(Session, { user: userId });
@@ -280,7 +284,7 @@ const createUserCommand = {
280
284
  soft: false
281
285
  });
282
286
  }
283
- ], { transaction: true });
287
+ ], { transaction: true, label: "auth.users.create.undo" });
284
288
  await emitCrudUndoSideEffects({
285
289
  dataEngine: de,
286
290
  action: "deleted",
@@ -425,6 +429,12 @@ const updateUserCommand = {
425
429
  async execute(rawInput, ctx) {
426
430
  const { parsed, custom } = parseWithCustomFields(updateSchema, rawInput);
427
431
  const em = ctx.container.resolve("em");
432
+ const actorTenantScope = resolveActorTenantScope(ctx);
433
+ const existing = await findOneWithDecryption(em, User, { id: parsed.id, deletedAt: null }, {}, { tenantId: null, organizationId: null });
434
+ if (!existing) throw new CrudHttpError(404, { error: "User not found" });
435
+ if (actorTenantScope && existing.tenantId && String(existing.tenantId) !== actorTenantScope) {
436
+ throw new CrudHttpError(404, { error: "User not found" });
437
+ }
428
438
  const rolesBefore = Array.isArray(parsed.roles) ? await loadUserRoleNames(em, parsed.id) : null;
429
439
  let tenantId;
430
440
  if (parsed.organizationId !== void 0) {
@@ -438,22 +448,9 @@ const updateUserCommand = {
438
448
  if (!organization) throw new CrudHttpError(400, { error: "Organization not found" });
439
449
  tenantId = organization.tenant?.id ? String(organization.tenant.id) : null;
440
450
  }
441
- if (parsed.email !== void 0) {
442
- const targetTenantId = tenantId !== void 0 ? tenantId : await resolveUserTenantId(em, parsed.id);
443
- const duplicate = await findOneWithDecryption(
444
- em,
445
- User,
446
- {
447
- $or: [{ email: parsed.email }, { emailHash: { $in: emailHashLookupValues(parsed.email) } }],
448
- deletedAt: null,
449
- tenantId: targetTenantId,
450
- id: { $ne: parsed.id }
451
- },
452
- {},
453
- { tenantId: null, organizationId: null }
454
- );
455
- if (duplicate) await throwDuplicateEmailError();
456
- }
451
+ const userTenantId = existing.tenantId ? String(existing.tenantId) : null;
452
+ const targetTenantId = tenantId !== void 0 ? tenantId : userTenantId;
453
+ const isTenantChanging = targetTenantId !== userTenantId;
457
454
  let hashed = null;
458
455
  let emailHash = null;
459
456
  if (parsed.password) {
@@ -463,49 +460,75 @@ const updateUserCommand = {
463
460
  if (parsed.email !== void 0) {
464
461
  emailHash = computeEmailHash(parsed.email);
465
462
  }
466
- const actorTenantScope = resolveActorTenantScope(ctx);
467
463
  const updateWhere = { id: parsed.id, deletedAt: null };
468
464
  if (actorTenantScope) updateWhere.tenantId = actorTenantScope;
469
465
  const de = ctx.container.resolve("dataEngine");
470
466
  let user;
471
- try {
472
- user = await de.updateOrmEntity({
473
- entity: User,
474
- where: updateWhere,
475
- apply: (entity) => {
476
- if (parsed.email !== void 0) {
477
- entity.email = parsed.email;
478
- entity.emailHash = emailHash;
479
- }
480
- if (parsed.name !== void 0) {
481
- entity.name = parsed.name;
482
- }
483
- if (parsed.organizationId !== void 0) {
484
- entity.organizationId = parsed.organizationId;
485
- entity.tenantId = tenantId ?? null;
486
- }
487
- if (hashed) entity.passwordHash = hashed;
467
+ await withAtomicFlush(em, [
468
+ async () => {
469
+ await enforceProtectedRoleFloor(em, userTenantId, parsed.id, {
470
+ deactivating: parsed.isConfirmed === false || isTenantChanging,
471
+ newRoles: parsed.roles
472
+ }, ctx);
473
+ if (parsed.email !== void 0) {
474
+ const duplicate = await findOneWithDecryption(
475
+ em,
476
+ User,
477
+ {
478
+ $or: [{ email: parsed.email }, { emailHash: { $in: emailHashLookupValues(parsed.email) } }],
479
+ deletedAt: null,
480
+ tenantId: targetTenantId,
481
+ id: { $ne: parsed.id }
482
+ },
483
+ {},
484
+ { tenantId: null, organizationId: null }
485
+ );
486
+ if (duplicate) await throwDuplicateEmailError();
488
487
  }
489
- });
490
- } catch (error) {
491
- if (isUniqueViolation(error)) await throwDuplicateEmailError();
492
- throw error;
493
- }
494
- if (!user) throw new CrudHttpError(404, { error: "User not found" });
495
- if (hashed) {
496
- await em.nativeDelete(Session, { user: parsed.id });
497
- }
498
- if (Array.isArray(parsed.roles)) {
499
- await syncUserRoles(em, user, parsed.roles, user.tenantId ? String(user.tenantId) : tenantId ?? null);
500
- }
501
- await setCustomFieldsIfAny({
502
- dataEngine: de,
503
- entityId: E.auth.user,
504
- recordId: String(user.id),
505
- organizationId: user.organizationId ? String(user.organizationId) : null,
506
- tenantId: user.tenantId ? String(user.tenantId) : tenantId ?? null,
507
- values: custom
508
- });
488
+ try {
489
+ const updated = await de.updateOrmEntity({
490
+ entity: User,
491
+ where: updateWhere,
492
+ apply: (entity) => {
493
+ if (parsed.email !== void 0) {
494
+ entity.email = parsed.email;
495
+ entity.emailHash = emailHash;
496
+ }
497
+ if (parsed.name !== void 0) {
498
+ entity.name = parsed.name;
499
+ }
500
+ if (parsed.organizationId !== void 0) {
501
+ entity.organizationId = parsed.organizationId;
502
+ entity.tenantId = tenantId ?? null;
503
+ }
504
+ if (parsed.isConfirmed !== void 0) {
505
+ entity.isConfirmed = parsed.isConfirmed;
506
+ }
507
+ if (hashed) entity.passwordHash = hashed;
508
+ }
509
+ });
510
+ if (updated) user = updated;
511
+ } catch (error) {
512
+ if (isUniqueViolation(error)) await throwDuplicateEmailError();
513
+ throw error;
514
+ }
515
+ if (!user) throw new CrudHttpError(404, { error: "User not found" });
516
+ if (hashed || parsed.isConfirmed === false) {
517
+ await em.nativeDelete(Session, { user: parsed.id });
518
+ }
519
+ if (Array.isArray(parsed.roles)) {
520
+ await syncUserRoles(em, user, parsed.roles, user.tenantId ? String(user.tenantId) : tenantId ?? null);
521
+ }
522
+ await setCustomFieldsIfAny({
523
+ dataEngine: de,
524
+ entityId: E.auth.user,
525
+ recordId: String(user.id),
526
+ organizationId: user.organizationId ? String(user.organizationId) : null,
527
+ tenantId: user.tenantId ? String(user.tenantId) : tenantId ?? null,
528
+ values: custom
529
+ });
530
+ }
531
+ ], { transaction: true });
509
532
  const identifiers = {
510
533
  id: String(user.id),
511
534
  organizationId: user.organizationId ? String(user.organizationId) : null,
@@ -588,22 +611,33 @@ const updateUserCommand = {
588
611
  const userId = before.id;
589
612
  const em = ctx.container.resolve("em");
590
613
  const de = ctx.container.resolve("dataEngine");
591
- const updated = await de.updateOrmEntity({
592
- entity: User,
593
- where: { id: userId, deletedAt: null },
594
- apply: (entity) => {
595
- entity.email = before.email;
596
- entity.organizationId = before.organizationId ?? null;
597
- entity.tenantId = before.tenantId ?? null;
598
- entity.passwordHash = before.passwordHash ?? null;
599
- entity.name = before.name ?? null;
600
- entity.isConfirmed = before.isConfirmed;
614
+ const restoredTenantId = before.tenantId ? String(before.tenantId) : null;
615
+ let updated = null;
616
+ await withAtomicFlush(em, [
617
+ async () => {
618
+ const current = await findOneWithDecryption(em, User, { id: userId, deletedAt: null }, {}, { tenantId: null, organizationId: null });
619
+ const currentTenantId = current?.tenantId ? String(current.tenantId) : null;
620
+ await enforceProtectedRoleFloor(em, currentTenantId, userId, {
621
+ deactivating: before.isConfirmed === false || restoredTenantId !== currentTenantId,
622
+ newRoles: before.roles
623
+ }, ctx);
624
+ updated = await de.updateOrmEntity({
625
+ entity: User,
626
+ where: { id: userId, deletedAt: null },
627
+ apply: (entity) => {
628
+ entity.email = before.email;
629
+ entity.organizationId = before.organizationId ?? null;
630
+ entity.tenantId = before.tenantId ?? null;
631
+ entity.passwordHash = before.passwordHash ?? null;
632
+ entity.name = before.name ?? null;
633
+ entity.isConfirmed = before.isConfirmed;
634
+ }
635
+ });
636
+ if (updated) {
637
+ await syncUserRoles(em, updated, before.roles, before.tenantId);
638
+ }
601
639
  }
602
- });
603
- if (updated) {
604
- await syncUserRoles(em, updated, before.roles, before.tenantId);
605
- await em.flush();
606
- }
640
+ ], { transaction: true, label: "auth.users.update.undo" });
607
641
  const reset = buildCustomFieldResetMap(before.custom, after?.custom);
608
642
  if (Object.keys(reset).length) {
609
643
  await setCustomFieldsIfAny({
@@ -658,11 +692,18 @@ const deleteUserCommand = {
658
692
  const em = ctx.container.resolve("em");
659
693
  const de = ctx.container.resolve("dataEngine");
660
694
  const actorTenantScope = resolveActorTenantScope(ctx);
695
+ const existing = await findOneWithDecryption(em, User, { id, deletedAt: null }, {}, { tenantId: null, organizationId: null });
696
+ if (!existing) throw new CrudHttpError(404, { error: "User not found" });
697
+ if (actorTenantScope && existing.tenantId && String(existing.tenantId) !== actorTenantScope) {
698
+ throw new CrudHttpError(404, { error: "User not found" });
699
+ }
661
700
  const deleteWhere = { id, deletedAt: null };
662
701
  if (actorTenantScope) deleteWhere.tenantId = actorTenantScope;
663
702
  let user;
664
703
  await withAtomicFlush(em, [
665
704
  async () => {
705
+ const userTenantId = existing.tenantId ? String(existing.tenantId) : null;
706
+ await enforceProtectedRoleFloor(em, userTenantId, id, { deleting: true }, ctx);
666
707
  await em.nativeDelete(UserAcl, { user: id });
667
708
  await em.nativeDelete(UserRole, { user: id });
668
709
  await em.nativeDelete(Session, { user: id });
@@ -913,10 +954,6 @@ function arrayEquals(left, right) {
913
954
  if (left.length !== right.length) return false;
914
955
  return left.every((value, idx) => value === right[idx]);
915
956
  }
916
- async function resolveUserTenantId(em, id) {
917
- const existing = await findOneWithDecryption(em, User, { id, deletedAt: null }, {}, { tenantId: null, organizationId: null });
918
- return existing?.tenantId ? String(existing.tenantId) : null;
919
- }
920
957
  async function throwDuplicateEmailError() {
921
958
  const { translate } = await resolveTranslations();
922
959
  const message = translate("auth.users.errors.emailExists", "Email already in use");
@@ -926,6 +963,68 @@ async function throwDuplicateEmailError() {
926
963
  details: [{ path: ["email"], message, code: "duplicate", origin: "validation" }]
927
964
  });
928
965
  }
966
+ function couldReduceActiveHolders(options) {
967
+ return options.deleting === true || options.deactivating === true || options.newRoles !== void 0;
968
+ }
969
+ async function enforceProtectedRoleFloor(em, tenantId, userId, options, ctx) {
970
+ if (ctx?.systemActor === true) return;
971
+ if (!couldReduceActiveHolders(options)) return;
972
+ const normalizedTenantId = normalizeTenantId(tenantId) ?? null;
973
+ if (!normalizedTenantId) return;
974
+ const protectedRoles = await findWithDecryption(em, Role, {
975
+ tenantId: normalizedTenantId,
976
+ minActiveHolders: { $gt: 0 },
977
+ deletedAt: null
978
+ }, {
979
+ lockMode: LockMode.PESSIMISTIC_WRITE,
980
+ orderBy: { id: "ASC" }
981
+ }, { tenantId: normalizedTenantId, organizationId: null });
982
+ if (protectedRoles.length === 0) return;
983
+ const { translate } = await resolveTranslations();
984
+ for (const role of protectedRoles) {
985
+ const minFloor = role.minActiveHolders ?? 0;
986
+ if (minFloor <= 0) continue;
987
+ const activeLinks = await findWithDecryption(em, UserRole, {
988
+ role: role.id,
989
+ deletedAt: null,
990
+ user: {
991
+ deletedAt: null,
992
+ isConfirmed: true,
993
+ tenantId: normalizedTenantId
994
+ }
995
+ }, {}, { tenantId: null, organizationId: null });
996
+ const activeUserIds = Array.from(
997
+ new Set(
998
+ activeLinks.map((link) => {
999
+ const userRef = link.user;
1000
+ const linkedUserId = typeof userRef === "string" ? userRef : userRef?.id;
1001
+ return linkedUserId ? String(linkedUserId) : null;
1002
+ }).filter((id) => !!id)
1003
+ )
1004
+ );
1005
+ if (activeUserIds.includes(userId)) {
1006
+ let willStillBeActiveHolder = true;
1007
+ if (options.deleting || options.deactivating) {
1008
+ willStillBeActiveHolder = false;
1009
+ } else if (options.newRoles !== void 0) {
1010
+ const desiredUnique = Array.from(new Set(options.newRoles.map((r) => r.trim().toLowerCase()).filter(Boolean)));
1011
+ const roleNameLower = role.name.toLowerCase();
1012
+ const hasRoleByName = desiredUnique.includes(roleNameLower) || desiredUnique.includes(String(role.id).toLowerCase());
1013
+ if (!hasRoleByName) {
1014
+ willStillBeActiveHolder = false;
1015
+ }
1016
+ }
1017
+ if (!willStillBeActiveHolder) {
1018
+ const remaining = activeUserIds.length - 1;
1019
+ if (remaining < minFloor) {
1020
+ throw new CrudHttpError(400, {
1021
+ error: translate("auth.users.errors.lastHolderOfCriticalRole", 'Cannot remove the last active holder of role "{roleName}"', { roleName: role.name })
1022
+ });
1023
+ }
1024
+ }
1025
+ }
1026
+ }
1027
+ }
929
1028
  export {
930
1029
  userCrudEvents,
931
1030
  userCrudIndexer