@open-mercato/enterprise 0.4.6-develop-15c18897fc → 0.4.6-develop-34aa847ce6

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 (195) hide show
  1. package/dist/index.js +1 -1
  2. package/dist/index.js.map +2 -2
  3. package/dist/modules/sso/acl.js +11 -0
  4. package/dist/modules/sso/acl.js.map +7 -0
  5. package/dist/modules/sso/api/admin-context.js +27 -0
  6. package/dist/modules/sso/api/admin-context.js.map +7 -0
  7. package/dist/modules/sso/api/callback/oidc/route.js +103 -0
  8. package/dist/modules/sso/api/callback/oidc/route.js.map +7 -0
  9. package/dist/modules/sso/api/config/[id]/activate/route.js +49 -0
  10. package/dist/modules/sso/api/config/[id]/activate/route.js.map +7 -0
  11. package/dist/modules/sso/api/config/[id]/domains/route.js +96 -0
  12. package/dist/modules/sso/api/config/[id]/domains/route.js.map +7 -0
  13. package/dist/modules/sso/api/config/[id]/route.js +103 -0
  14. package/dist/modules/sso/api/config/[id]/route.js.map +7 -0
  15. package/dist/modules/sso/api/config/[id]/test/route.js +41 -0
  16. package/dist/modules/sso/api/config/[id]/test/route.js.map +7 -0
  17. package/dist/modules/sso/api/config/route.js +83 -0
  18. package/dist/modules/sso/api/config/route.js.map +7 -0
  19. package/dist/modules/sso/api/error-handler.js +28 -0
  20. package/dist/modules/sso/api/error-handler.js.map +7 -0
  21. package/dist/modules/sso/api/hrd/route.js +52 -0
  22. package/dist/modules/sso/api/hrd/route.js.map +7 -0
  23. package/dist/modules/sso/api/initiate/route.js +66 -0
  24. package/dist/modules/sso/api/initiate/route.js.map +7 -0
  25. package/dist/modules/sso/api/scim/context.js +68 -0
  26. package/dist/modules/sso/api/scim/context.js.map +7 -0
  27. package/dist/modules/sso/api/scim/logs/route.js +65 -0
  28. package/dist/modules/sso/api/scim/logs/route.js.map +7 -0
  29. package/dist/modules/sso/api/scim/tokens/[id]/route.js +42 -0
  30. package/dist/modules/sso/api/scim/tokens/[id]/route.js.map +7 -0
  31. package/dist/modules/sso/api/scim/tokens/route.js +83 -0
  32. package/dist/modules/sso/api/scim/tokens/route.js.map +7 -0
  33. package/dist/modules/sso/api/scim/v2/ServiceProviderConfig/route.js +42 -0
  34. package/dist/modules/sso/api/scim/v2/ServiceProviderConfig/route.js.map +7 -0
  35. package/dist/modules/sso/api/scim/v2/Users/[id]/route.js +94 -0
  36. package/dist/modules/sso/api/scim/v2/Users/[id]/route.js.map +7 -0
  37. package/dist/modules/sso/api/scim/v2/Users/route.js +86 -0
  38. package/dist/modules/sso/api/scim/v2/Users/route.js.map +7 -0
  39. package/dist/modules/sso/backend/page.js +173 -0
  40. package/dist/modules/sso/backend/page.js.map +7 -0
  41. package/dist/modules/sso/backend/page.meta.js +31 -0
  42. package/dist/modules/sso/backend/page.meta.js.map +7 -0
  43. package/dist/modules/sso/backend/sso/config/[id]/page.js +749 -0
  44. package/dist/modules/sso/backend/sso/config/[id]/page.js.map +7 -0
  45. package/dist/modules/sso/backend/sso/config/[id]/page.meta.js +19 -0
  46. package/dist/modules/sso/backend/sso/config/[id]/page.meta.js.map +7 -0
  47. package/dist/modules/sso/backend/sso/config/new/page.js +381 -0
  48. package/dist/modules/sso/backend/sso/config/new/page.js.map +7 -0
  49. package/dist/modules/sso/backend/sso/config/new/page.meta.js +19 -0
  50. package/dist/modules/sso/backend/sso/config/new/page.meta.js.map +7 -0
  51. package/dist/modules/sso/data/entities.js +299 -0
  52. package/dist/modules/sso/data/entities.js.map +7 -0
  53. package/dist/modules/sso/data/validators.js +114 -0
  54. package/dist/modules/sso/data/validators.js.map +7 -0
  55. package/dist/modules/sso/di.js +26 -0
  56. package/dist/modules/sso/di.js.map +7 -0
  57. package/dist/modules/sso/events.js +24 -0
  58. package/dist/modules/sso/events.js.map +7 -0
  59. package/dist/modules/sso/i18n/de.json +146 -0
  60. package/dist/modules/sso/i18n/en.json +146 -0
  61. package/dist/modules/sso/i18n/es.json +146 -0
  62. package/dist/modules/sso/i18n/pl.json +146 -0
  63. package/dist/modules/sso/index.js +11 -0
  64. package/dist/modules/sso/index.js.map +7 -0
  65. package/dist/modules/sso/lib/domains.js +30 -0
  66. package/dist/modules/sso/lib/domains.js.map +7 -0
  67. package/dist/modules/sso/lib/oidc-provider.js +140 -0
  68. package/dist/modules/sso/lib/oidc-provider.js.map +7 -0
  69. package/dist/modules/sso/lib/registry.js +15 -0
  70. package/dist/modules/sso/lib/registry.js.map +7 -0
  71. package/dist/modules/sso/lib/scim-filter.js +43 -0
  72. package/dist/modules/sso/lib/scim-filter.js.map +7 -0
  73. package/dist/modules/sso/lib/scim-mapper.js +49 -0
  74. package/dist/modules/sso/lib/scim-mapper.js.map +7 -0
  75. package/dist/modules/sso/lib/scim-patch.js +63 -0
  76. package/dist/modules/sso/lib/scim-patch.js.map +7 -0
  77. package/dist/modules/sso/lib/scim-response.js +34 -0
  78. package/dist/modules/sso/lib/scim-response.js.map +7 -0
  79. package/dist/modules/sso/lib/scim-utils.js +9 -0
  80. package/dist/modules/sso/lib/scim-utils.js.map +7 -0
  81. package/dist/modules/sso/lib/state-cookie.js +67 -0
  82. package/dist/modules/sso/lib/state-cookie.js.map +7 -0
  83. package/dist/modules/sso/lib/types.js +1 -0
  84. package/dist/modules/sso/lib/types.js.map +7 -0
  85. package/dist/modules/sso/migrations/Migration20260219000000_sso.js +20 -0
  86. package/dist/modules/sso/migrations/Migration20260219000000_sso.js.map +7 -0
  87. package/dist/modules/sso/migrations/Migration20260222000000_sso_add_name.js +13 -0
  88. package/dist/modules/sso/migrations/Migration20260222000000_sso_add_name.js.map +7 -0
  89. package/dist/modules/sso/migrations/Migration20260222000001_sso_partial_unique_org.js +15 -0
  90. package/dist/modules/sso/migrations/Migration20260222000001_sso_partial_unique_org.js.map +7 -0
  91. package/dist/modules/sso/migrations/Migration20260223000000_scim_tables.js +22 -0
  92. package/dist/modules/sso/migrations/Migration20260223000000_scim_tables.js.map +7 -0
  93. package/dist/modules/sso/migrations/Migration20260224000000_sso_external_id.js +15 -0
  94. package/dist/modules/sso/migrations/Migration20260224000000_sso_external_id.js.map +7 -0
  95. package/dist/modules/sso/migrations/Migration20260224100000_sso_role_grants.js +17 -0
  96. package/dist/modules/sso/migrations/Migration20260224100000_sso_role_grants.js.map +7 -0
  97. package/dist/modules/sso/migrations/Migration20260224200000_drop_default_role_id.js +13 -0
  98. package/dist/modules/sso/migrations/Migration20260224200000_drop_default_role_id.js.map +7 -0
  99. package/dist/modules/sso/migrations/Migration20260225000000_sso_identities_partial_unique.js +23 -0
  100. package/dist/modules/sso/migrations/Migration20260225000000_sso_identities_partial_unique.js.map +7 -0
  101. package/dist/modules/sso/migrations/Migration20260305000000_sso_role_grants_org_id.js +14 -0
  102. package/dist/modules/sso/migrations/Migration20260305000000_sso_role_grants_org_id.js.map +7 -0
  103. package/dist/modules/sso/services/accountLinkingService.js +298 -0
  104. package/dist/modules/sso/services/accountLinkingService.js.map +7 -0
  105. package/dist/modules/sso/services/hrdService.js +18 -0
  106. package/dist/modules/sso/services/hrdService.js.map +7 -0
  107. package/dist/modules/sso/services/scimService.js +372 -0
  108. package/dist/modules/sso/services/scimService.js.map +7 -0
  109. package/dist/modules/sso/services/scimTokenService.js +94 -0
  110. package/dist/modules/sso/services/scimTokenService.js.map +7 -0
  111. package/dist/modules/sso/services/ssoConfigService.js +254 -0
  112. package/dist/modules/sso/services/ssoConfigService.js.map +7 -0
  113. package/dist/modules/sso/services/ssoService.js +125 -0
  114. package/dist/modules/sso/services/ssoService.js.map +7 -0
  115. package/dist/modules/sso/setup.js +47 -0
  116. package/dist/modules/sso/setup.js.map +7 -0
  117. package/dist/modules/sso/subscribers/user-deleted-cleanup.js +21 -0
  118. package/dist/modules/sso/subscribers/user-deleted-cleanup.js.map +7 -0
  119. package/dist/modules/sso/widgets/injection/login-sso/widget.client.js +106 -0
  120. package/dist/modules/sso/widgets/injection/login-sso/widget.client.js.map +7 -0
  121. package/dist/modules/sso/widgets/injection/login-sso/widget.js +16 -0
  122. package/dist/modules/sso/widgets/injection/login-sso/widget.js.map +7 -0
  123. package/dist/modules/sso/widgets/injection-table.js +14 -0
  124. package/dist/modules/sso/widgets/injection-table.js.map +7 -0
  125. package/package.json +5 -4
  126. package/src/index.ts +1 -1
  127. package/src/modules/sso/acl.ts +7 -0
  128. package/src/modules/sso/api/admin-context.ts +36 -0
  129. package/src/modules/sso/api/callback/oidc/route.ts +115 -0
  130. package/src/modules/sso/api/config/[id]/activate/route.ts +53 -0
  131. package/src/modules/sso/api/config/[id]/domains/route.ts +107 -0
  132. package/src/modules/sso/api/config/[id]/route.ts +114 -0
  133. package/src/modules/sso/api/config/[id]/test/route.ts +44 -0
  134. package/src/modules/sso/api/config/route.ts +88 -0
  135. package/src/modules/sso/api/error-handler.ts +36 -0
  136. package/src/modules/sso/api/hrd/route.ts +55 -0
  137. package/src/modules/sso/api/initiate/route.ts +70 -0
  138. package/src/modules/sso/api/scim/context.ts +85 -0
  139. package/src/modules/sso/api/scim/logs/route.ts +69 -0
  140. package/src/modules/sso/api/scim/tokens/[id]/route.ts +45 -0
  141. package/src/modules/sso/api/scim/tokens/route.ts +89 -0
  142. package/src/modules/sso/api/scim/v2/ServiceProviderConfig/route.ts +40 -0
  143. package/src/modules/sso/api/scim/v2/Users/[id]/route.ts +103 -0
  144. package/src/modules/sso/api/scim/v2/Users/route.ts +94 -0
  145. package/src/modules/sso/backend/page.meta.ts +29 -0
  146. package/src/modules/sso/backend/page.tsx +232 -0
  147. package/src/modules/sso/backend/sso/config/[id]/page.meta.ts +15 -0
  148. package/src/modules/sso/backend/sso/config/[id]/page.tsx +1024 -0
  149. package/src/modules/sso/backend/sso/config/new/page.meta.ts +15 -0
  150. package/src/modules/sso/backend/sso/config/new/page.tsx +463 -0
  151. package/src/modules/sso/data/entities.ts +240 -0
  152. package/src/modules/sso/data/validators.ts +140 -0
  153. package/src/modules/sso/di.ts +25 -0
  154. package/src/modules/sso/docs/entra-id-setup.md +281 -0
  155. package/src/modules/sso/docs/google-workspace-setup.md +174 -0
  156. package/src/modules/sso/docs/sso-overview.md +218 -0
  157. package/src/modules/sso/docs/sso-security-audit-2026-02-27.md +118 -0
  158. package/src/modules/sso/docs/zitadel-setup.md +195 -0
  159. package/src/modules/sso/events.ts +21 -0
  160. package/src/modules/sso/i18n/de.json +146 -0
  161. package/src/modules/sso/i18n/en.json +146 -0
  162. package/src/modules/sso/i18n/es.json +146 -0
  163. package/src/modules/sso/i18n/pl.json +146 -0
  164. package/src/modules/sso/index.ts +7 -0
  165. package/src/modules/sso/lib/domains.ts +31 -0
  166. package/src/modules/sso/lib/oidc-provider.ts +196 -0
  167. package/src/modules/sso/lib/registry.ts +13 -0
  168. package/src/modules/sso/lib/scim-filter.ts +62 -0
  169. package/src/modules/sso/lib/scim-mapper.ts +88 -0
  170. package/src/modules/sso/lib/scim-patch.ts +88 -0
  171. package/src/modules/sso/lib/scim-response.ts +40 -0
  172. package/src/modules/sso/lib/scim-utils.ts +5 -0
  173. package/src/modules/sso/lib/state-cookie.ts +79 -0
  174. package/src/modules/sso/lib/types.ts +50 -0
  175. package/src/modules/sso/migrations/.snapshot-open-mercato.json +912 -0
  176. package/src/modules/sso/migrations/Migration20260219000000_sso.ts +21 -0
  177. package/src/modules/sso/migrations/Migration20260222000000_sso_add_name.ts +13 -0
  178. package/src/modules/sso/migrations/Migration20260222000001_sso_partial_unique_org.ts +15 -0
  179. package/src/modules/sso/migrations/Migration20260223000000_scim_tables.ts +24 -0
  180. package/src/modules/sso/migrations/Migration20260224000000_sso_external_id.ts +15 -0
  181. package/src/modules/sso/migrations/Migration20260224100000_sso_role_grants.ts +18 -0
  182. package/src/modules/sso/migrations/Migration20260224200000_drop_default_role_id.ts +13 -0
  183. package/src/modules/sso/migrations/Migration20260225000000_sso_identities_partial_unique.ts +25 -0
  184. package/src/modules/sso/migrations/Migration20260305000000_sso_role_grants_org_id.ts +14 -0
  185. package/src/modules/sso/services/accountLinkingService.ts +386 -0
  186. package/src/modules/sso/services/hrdService.ts +22 -0
  187. package/src/modules/sso/services/scimService.ts +461 -0
  188. package/src/modules/sso/services/scimTokenService.ts +136 -0
  189. package/src/modules/sso/services/ssoConfigService.ts +337 -0
  190. package/src/modules/sso/services/ssoService.ts +167 -0
  191. package/src/modules/sso/setup.ts +56 -0
  192. package/src/modules/sso/subscribers/user-deleted-cleanup.ts +33 -0
  193. package/src/modules/sso/widgets/injection/login-sso/widget.client.tsx +130 -0
  194. package/src/modules/sso/widgets/injection/login-sso/widget.ts +16 -0
  195. package/src/modules/sso/widgets/injection-table.ts +12 -0
@@ -0,0 +1,254 @@
1
+ import { escapeLikePattern } from "@open-mercato/shared/lib/db/escapeLikePattern";
2
+ import { SsoConfig, ScimToken } from "../data/entities.js";
3
+ import { emitSsoEvent } from "../events.js";
4
+ import { validateDomain, normalizeDomain, uniqueDomains, checkDomainLimit } from "../lib/domains.js";
5
+ class SsoConfigService {
6
+ constructor(em, tenantEncryptionService, ssoProviderRegistry) {
7
+ this.em = em;
8
+ this.tenantEncryptionService = tenantEncryptionService;
9
+ this.ssoProviderRegistry = ssoProviderRegistry;
10
+ }
11
+ async list(scope, query) {
12
+ const where = { deletedAt: null };
13
+ if (!scope.isSuperAdmin) {
14
+ if (!scope.organizationId) {
15
+ throw new SsoConfigError("Organization context is required", 403);
16
+ }
17
+ where.organizationId = scope.organizationId;
18
+ } else {
19
+ if (query.organizationId) where.organizationId = query.organizationId;
20
+ if (query.tenantId) where.tenantId = query.tenantId;
21
+ }
22
+ if (query.search) {
23
+ const pattern = `%${escapeLikePattern(query.search)}%`;
24
+ where.$or = [
25
+ { name: { $ilike: pattern } },
26
+ { issuer: { $ilike: pattern } },
27
+ { clientId: { $ilike: pattern } }
28
+ ];
29
+ }
30
+ const [configs, total] = await this.em.findAndCount(SsoConfig, where, {
31
+ orderBy: { createdAt: "desc" },
32
+ limit: query.pageSize,
33
+ offset: (query.page - 1) * query.pageSize
34
+ });
35
+ return {
36
+ items: configs.map((c) => this.toPublic(c)),
37
+ total,
38
+ totalPages: Math.ceil(total / query.pageSize) || 1
39
+ };
40
+ }
41
+ async getById(scope, id) {
42
+ const where = { id, deletedAt: null };
43
+ if (!scope.isSuperAdmin) {
44
+ if (!scope.organizationId) throw new SsoConfigError("Organization context is required", 403);
45
+ where.organizationId = scope.organizationId;
46
+ }
47
+ const config = await this.em.findOne(SsoConfig, where);
48
+ return config ? this.toPublic(config) : null;
49
+ }
50
+ async create(scope, input) {
51
+ const orgId = scope.isSuperAdmin ? input.organizationId : scope.organizationId;
52
+ const tenId = scope.isSuperAdmin ? input.tenantId ?? null : scope.tenantId;
53
+ const existing = await this.em.findOne(SsoConfig, {
54
+ organizationId: orgId,
55
+ deletedAt: null
56
+ });
57
+ if (existing) {
58
+ throw new SsoConfigError("An SSO configuration already exists for this organization", 409);
59
+ }
60
+ const domains = uniqueDomains(input.allowedDomains);
61
+ for (const d of domains) {
62
+ const result = validateDomain(d);
63
+ if (!result.valid) throw new SsoConfigError(`Invalid domain "${d}": ${result.error}`, 400);
64
+ }
65
+ const encrypted = await this.tenantEncryptionService.encryptEntityPayload(
66
+ "SsoConfig",
67
+ { clientSecretEnc: input.clientSecret },
68
+ tenId,
69
+ orgId
70
+ );
71
+ const config = this.em.create(SsoConfig, {
72
+ name: input.name,
73
+ tenantId: tenId,
74
+ organizationId: orgId,
75
+ protocol: input.protocol,
76
+ issuer: input.issuer,
77
+ clientId: input.clientId,
78
+ clientSecretEnc: encrypted.clientSecretEnc,
79
+ allowedDomains: domains,
80
+ jitEnabled: input.jitEnabled,
81
+ autoLinkByEmail: input.autoLinkByEmail,
82
+ isActive: false,
83
+ ssoRequired: false,
84
+ appRoleMappings: input.appRoleMappings ?? {}
85
+ });
86
+ await this.em.persistAndFlush(config);
87
+ void emitSsoEvent("sso.config.created", {
88
+ id: config.id,
89
+ tenantId: config.tenantId,
90
+ organizationId: config.organizationId
91
+ }).catch((e) => console.error("[SSO Event]", e));
92
+ return this.toPublic(config);
93
+ }
94
+ async update(scope, id, input) {
95
+ const config = await this.resolveConfig(scope, id);
96
+ if (input.name !== void 0) config.name = input.name;
97
+ if (input.protocol !== void 0) config.protocol = input.protocol;
98
+ if (input.issuer !== void 0) config.issuer = input.issuer;
99
+ if (input.clientId !== void 0) config.clientId = input.clientId;
100
+ if (input.jitEnabled !== void 0) {
101
+ if (input.jitEnabled) {
102
+ const activeScimCount = await this.em.count(ScimToken, { ssoConfigId: id, isActive: true });
103
+ if (activeScimCount > 0) {
104
+ throw new SsoConfigError("Cannot enable JIT provisioning while SCIM directory sync is active. Revoke all SCIM tokens first.", 409);
105
+ }
106
+ }
107
+ config.jitEnabled = input.jitEnabled;
108
+ }
109
+ if (input.autoLinkByEmail !== void 0) config.autoLinkByEmail = input.autoLinkByEmail;
110
+ if (input.appRoleMappings !== void 0) config.appRoleMappings = input.appRoleMappings;
111
+ if (input.clientSecret !== void 0) {
112
+ const encrypted = await this.tenantEncryptionService.encryptEntityPayload(
113
+ "SsoConfig",
114
+ { clientSecretEnc: input.clientSecret },
115
+ config.tenantId,
116
+ config.organizationId
117
+ );
118
+ config.clientSecretEnc = encrypted.clientSecretEnc;
119
+ }
120
+ await this.em.flush();
121
+ void emitSsoEvent("sso.config.updated", {
122
+ id: config.id,
123
+ tenantId: config.tenantId,
124
+ organizationId: config.organizationId
125
+ }).catch((e) => console.error("[SSO Event]", e));
126
+ return this.toPublic(config);
127
+ }
128
+ async delete(scope, id) {
129
+ const config = await this.resolveConfig(scope, id);
130
+ if (config.isActive) {
131
+ throw new SsoConfigError("Cannot delete an active SSO configuration \u2014 deactivate it first", 400);
132
+ }
133
+ config.deletedAt = /* @__PURE__ */ new Date();
134
+ await this.em.flush();
135
+ void emitSsoEvent("sso.config.deleted", {
136
+ id: config.id,
137
+ tenantId: config.tenantId,
138
+ organizationId: config.organizationId
139
+ }).catch((e) => console.error("[SSO Event]", e));
140
+ }
141
+ async activate(scope, id, active) {
142
+ const config = await this.resolveConfig(scope, id);
143
+ if (active) {
144
+ if (config.allowedDomains.length === 0) {
145
+ throw new SsoConfigError("Cannot activate SSO configuration with no allowed domains", 400);
146
+ }
147
+ const testResult = await this.testConnectionInternal(config);
148
+ if (!testResult.ok) {
149
+ throw new SsoConfigError(`Cannot activate \u2014 discovery failed: ${testResult.error}`, 400);
150
+ }
151
+ }
152
+ const wasActive = config.isActive;
153
+ config.isActive = active;
154
+ await this.em.flush();
155
+ if (active && !wasActive) {
156
+ void emitSsoEvent("sso.config.activated", {
157
+ id: config.id,
158
+ tenantId: config.tenantId,
159
+ organizationId: config.organizationId
160
+ }).catch((e) => console.error("[SSO Event]", e));
161
+ } else if (!active && wasActive) {
162
+ void emitSsoEvent("sso.config.deactivated", {
163
+ id: config.id,
164
+ tenantId: config.tenantId,
165
+ organizationId: config.organizationId
166
+ }).catch((e) => console.error("[SSO Event]", e));
167
+ }
168
+ return this.toPublic(config);
169
+ }
170
+ async testConnection(scope, id) {
171
+ const config = await this.resolveConfig(scope, id);
172
+ return this.testConnectionInternal(config);
173
+ }
174
+ async addDomain(scope, id, domain) {
175
+ const normalized = normalizeDomain(domain);
176
+ const validation = validateDomain(normalized);
177
+ if (!validation.valid) throw new SsoConfigError(validation.error, 400);
178
+ const config = await this.resolveConfig(scope, id);
179
+ if (config.allowedDomains.includes(normalized)) {
180
+ return this.toPublic(config);
181
+ }
182
+ const limitCheck = checkDomainLimit(config.allowedDomains.length, 1);
183
+ if (!limitCheck.ok) throw new SsoConfigError(limitCheck.error, 400);
184
+ config.allowedDomains = [...config.allowedDomains, normalized];
185
+ await this.em.flush();
186
+ void emitSsoEvent("sso.domain.added", {
187
+ id: config.id,
188
+ tenantId: config.tenantId,
189
+ organizationId: config.organizationId,
190
+ domain: normalized
191
+ }).catch((e) => console.error("[SSO Event]", e));
192
+ return this.toPublic(config);
193
+ }
194
+ async removeDomain(scope, id, domain) {
195
+ const normalized = normalizeDomain(domain);
196
+ const config = await this.resolveConfig(scope, id);
197
+ config.allowedDomains = config.allowedDomains.filter((d) => d !== normalized);
198
+ await this.em.flush();
199
+ void emitSsoEvent("sso.domain.removed", {
200
+ id: config.id,
201
+ tenantId: config.tenantId,
202
+ organizationId: config.organizationId,
203
+ domain: normalized
204
+ }).catch((e) => console.error("[SSO Event]", e));
205
+ return this.toPublic(config);
206
+ }
207
+ toPublic(config) {
208
+ return {
209
+ id: config.id,
210
+ name: config.name ?? null,
211
+ tenantId: config.tenantId ?? null,
212
+ organizationId: config.organizationId,
213
+ protocol: config.protocol,
214
+ issuer: config.issuer ?? null,
215
+ clientId: config.clientId ?? null,
216
+ hasClientSecret: !!config.clientSecretEnc,
217
+ allowedDomains: config.allowedDomains,
218
+ jitEnabled: config.jitEnabled,
219
+ autoLinkByEmail: config.autoLinkByEmail,
220
+ isActive: config.isActive,
221
+ ssoRequired: config.ssoRequired,
222
+ appRoleMappings: config.appRoleMappings ?? {},
223
+ createdAt: config.createdAt,
224
+ updatedAt: config.updatedAt
225
+ };
226
+ }
227
+ async resolveConfig(scope, id) {
228
+ const where = { id, deletedAt: null };
229
+ if (!scope.isSuperAdmin) {
230
+ if (!scope.organizationId) throw new SsoConfigError("Organization context is required", 403);
231
+ where.organizationId = scope.organizationId;
232
+ }
233
+ const config = await this.em.findOne(SsoConfig, where);
234
+ if (!config) throw new SsoConfigError("SSO configuration not found", 404);
235
+ return config;
236
+ }
237
+ async testConnectionInternal(config) {
238
+ const provider = this.ssoProviderRegistry.resolve(config.protocol);
239
+ if (!provider) return { ok: false, error: `No provider for protocol: ${config.protocol}` };
240
+ return provider.validateConfig(config);
241
+ }
242
+ }
243
+ class SsoConfigError extends Error {
244
+ constructor(message, statusCode) {
245
+ super(message);
246
+ this.statusCode = statusCode;
247
+ this.name = "SsoConfigError";
248
+ }
249
+ }
250
+ export {
251
+ SsoConfigError,
252
+ SsoConfigService
253
+ };
254
+ //# sourceMappingURL=ssoConfigService.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/sso/services/ssoConfigService.ts"],
4
+ "sourcesContent": ["import type { FilterQuery, RequiredEntityData } from '@mikro-orm/core'\nimport { EntityManager } from '@mikro-orm/postgresql'\nimport { findWithDecryption, findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport type { TenantDataEncryptionService } from '@open-mercato/shared/lib/encryption/tenantDataEncryptionService'\nimport { escapeLikePattern } from '@open-mercato/shared/lib/db/escapeLikePattern'\nimport { SsoConfig, ScimToken } from '../data/entities'\nimport type { SsoConfigAdminCreateInput, SsoConfigAdminUpdateInput, SsoConfigListQuery } from '../data/validators'\nimport { emitSsoEvent } from '../events'\nimport { validateDomain, normalizeDomain, uniqueDomains, checkDomainLimit } from '../lib/domains'\nimport type { SsoProviderRegistry } from '../lib/registry'\n\nexport interface SsoAdminScope {\n isSuperAdmin: boolean\n organizationId: string | null\n tenantId: string | null\n}\n\nexport interface SsoConfigPublic {\n id: string\n name: string | null\n tenantId: string | null\n organizationId: string\n protocol: string\n issuer: string | null\n clientId: string | null\n hasClientSecret: boolean\n allowedDomains: string[]\n jitEnabled: boolean\n autoLinkByEmail: boolean\n isActive: boolean\n ssoRequired: boolean\n appRoleMappings: Record<string, string>\n createdAt: Date\n updatedAt: Date\n}\n\nexport class SsoConfigService {\n constructor(\n private em: EntityManager,\n private tenantEncryptionService: TenantDataEncryptionService,\n private ssoProviderRegistry: SsoProviderRegistry,\n ) {}\n\n async list(scope: SsoAdminScope, query: SsoConfigListQuery): Promise<{\n items: SsoConfigPublic[]\n total: number\n totalPages: number\n }> {\n const where: FilterQuery<SsoConfig> = { deletedAt: null }\n\n if (!scope.isSuperAdmin) {\n if (!scope.organizationId) {\n throw new SsoConfigError('Organization context is required', 403)\n }\n where.organizationId = scope.organizationId\n } else {\n if (query.organizationId) where.organizationId = query.organizationId\n if (query.tenantId) where.tenantId = query.tenantId\n }\n\n if (query.search) {\n const pattern = `%${escapeLikePattern(query.search)}%`\n where.$or = [\n { name: { $ilike: pattern } },\n { issuer: { $ilike: pattern } },\n { clientId: { $ilike: pattern } },\n ]\n }\n\n const [configs, total] = await this.em.findAndCount(SsoConfig, where, {\n orderBy: { createdAt: 'desc' },\n limit: query.pageSize,\n offset: (query.page - 1) * query.pageSize,\n })\n\n return {\n items: configs.map((c) => this.toPublic(c)),\n total,\n totalPages: Math.ceil(total / query.pageSize) || 1,\n }\n }\n\n async getById(scope: SsoAdminScope, id: string): Promise<SsoConfigPublic | null> {\n const where: FilterQuery<SsoConfig> = { id, deletedAt: null }\n if (!scope.isSuperAdmin) {\n if (!scope.organizationId) throw new SsoConfigError('Organization context is required', 403)\n where.organizationId = scope.organizationId\n }\n\n const config = await this.em.findOne(SsoConfig, where)\n return config ? this.toPublic(config) : null\n }\n\n async create(scope: SsoAdminScope, input: SsoConfigAdminCreateInput): Promise<SsoConfigPublic> {\n const orgId = scope.isSuperAdmin ? input.organizationId : scope.organizationId!\n const tenId = scope.isSuperAdmin ? (input.tenantId ?? null) : scope.tenantId\n\n const existing = await this.em.findOne(SsoConfig, {\n organizationId: orgId,\n deletedAt: null,\n })\n if (existing) {\n throw new SsoConfigError('An SSO configuration already exists for this organization', 409)\n }\n\n const domains = uniqueDomains(input.allowedDomains)\n for (const d of domains) {\n const result = validateDomain(d)\n if (!result.valid) throw new SsoConfigError(`Invalid domain \"${d}\": ${result.error}`, 400)\n }\n\n const encrypted = await this.tenantEncryptionService.encryptEntityPayload(\n 'SsoConfig',\n { clientSecretEnc: input.clientSecret },\n tenId,\n orgId,\n )\n\n const config = this.em.create(SsoConfig, {\n name: input.name,\n tenantId: tenId,\n organizationId: orgId,\n protocol: input.protocol,\n issuer: input.issuer,\n clientId: input.clientId,\n clientSecretEnc: encrypted.clientSecretEnc as string,\n allowedDomains: domains,\n jitEnabled: input.jitEnabled,\n autoLinkByEmail: input.autoLinkByEmail,\n isActive: false,\n ssoRequired: false,\n appRoleMappings: input.appRoleMappings ?? {},\n } as RequiredEntityData<SsoConfig>)\n\n await this.em.persistAndFlush(config)\n\n void emitSsoEvent('sso.config.created', {\n id: config.id,\n tenantId: config.tenantId,\n organizationId: config.organizationId,\n }).catch((e) => console.error('[SSO Event]', e))\n\n return this.toPublic(config)\n }\n\n async update(scope: SsoAdminScope, id: string, input: SsoConfigAdminUpdateInput): Promise<SsoConfigPublic> {\n const config = await this.resolveConfig(scope, id)\n\n if (input.name !== undefined) config.name = input.name\n if (input.protocol !== undefined) config.protocol = input.protocol\n if (input.issuer !== undefined) config.issuer = input.issuer\n if (input.clientId !== undefined) config.clientId = input.clientId\n if (input.jitEnabled !== undefined) {\n if (input.jitEnabled) {\n const activeScimCount = await this.em.count(ScimToken, { ssoConfigId: id, isActive: true })\n if (activeScimCount > 0) {\n throw new SsoConfigError('Cannot enable JIT provisioning while SCIM directory sync is active. Revoke all SCIM tokens first.', 409)\n }\n }\n config.jitEnabled = input.jitEnabled\n }\n if (input.autoLinkByEmail !== undefined) config.autoLinkByEmail = input.autoLinkByEmail\n if (input.appRoleMappings !== undefined) config.appRoleMappings = input.appRoleMappings\n\n if (input.clientSecret !== undefined) {\n const encrypted = await this.tenantEncryptionService.encryptEntityPayload(\n 'SsoConfig',\n { clientSecretEnc: input.clientSecret },\n config.tenantId,\n config.organizationId,\n )\n config.clientSecretEnc = encrypted.clientSecretEnc as string\n }\n\n await this.em.flush()\n\n void emitSsoEvent('sso.config.updated', {\n id: config.id,\n tenantId: config.tenantId,\n organizationId: config.organizationId,\n }).catch((e) => console.error('[SSO Event]', e))\n\n return this.toPublic(config)\n }\n\n async delete(scope: SsoAdminScope, id: string): Promise<void> {\n const config = await this.resolveConfig(scope, id)\n\n if (config.isActive) {\n throw new SsoConfigError('Cannot delete an active SSO configuration \u2014 deactivate it first', 400)\n }\n\n config.deletedAt = new Date()\n await this.em.flush()\n\n void emitSsoEvent('sso.config.deleted', {\n id: config.id,\n tenantId: config.tenantId,\n organizationId: config.organizationId,\n }).catch((e) => console.error('[SSO Event]', e))\n }\n\n async activate(scope: SsoAdminScope, id: string, active: boolean): Promise<SsoConfigPublic> {\n const config = await this.resolveConfig(scope, id)\n\n if (active) {\n if (config.allowedDomains.length === 0) {\n throw new SsoConfigError('Cannot activate SSO configuration with no allowed domains', 400)\n }\n\n const testResult = await this.testConnectionInternal(config)\n if (!testResult.ok) {\n throw new SsoConfigError(`Cannot activate \u2014 discovery failed: ${testResult.error}`, 400)\n }\n }\n\n const wasActive = config.isActive\n config.isActive = active\n await this.em.flush()\n\n if (active && !wasActive) {\n void emitSsoEvent('sso.config.activated', {\n id: config.id,\n tenantId: config.tenantId,\n organizationId: config.organizationId,\n }).catch((e) => console.error('[SSO Event]', e))\n } else if (!active && wasActive) {\n void emitSsoEvent('sso.config.deactivated', {\n id: config.id,\n tenantId: config.tenantId,\n organizationId: config.organizationId,\n }).catch((e) => console.error('[SSO Event]', e))\n }\n\n return this.toPublic(config)\n }\n\n async testConnection(scope: SsoAdminScope, id: string): Promise<{ ok: boolean; error?: string }> {\n const config = await this.resolveConfig(scope, id)\n return this.testConnectionInternal(config)\n }\n\n async addDomain(scope: SsoAdminScope, id: string, domain: string): Promise<SsoConfigPublic> {\n const normalized = normalizeDomain(domain)\n const validation = validateDomain(normalized)\n if (!validation.valid) throw new SsoConfigError(validation.error!, 400)\n\n const config = await this.resolveConfig(scope, id)\n\n if (config.allowedDomains.includes(normalized)) {\n return this.toPublic(config)\n }\n\n const limitCheck = checkDomainLimit(config.allowedDomains.length, 1)\n if (!limitCheck.ok) throw new SsoConfigError(limitCheck.error!, 400)\n\n config.allowedDomains = [...config.allowedDomains, normalized]\n await this.em.flush()\n\n void emitSsoEvent('sso.domain.added', {\n id: config.id,\n tenantId: config.tenantId,\n organizationId: config.organizationId,\n domain: normalized,\n }).catch((e) => console.error('[SSO Event]', e))\n\n return this.toPublic(config)\n }\n\n async removeDomain(scope: SsoAdminScope, id: string, domain: string): Promise<SsoConfigPublic> {\n const normalized = normalizeDomain(domain)\n const config = await this.resolveConfig(scope, id)\n\n config.allowedDomains = config.allowedDomains.filter((d) => d !== normalized)\n await this.em.flush()\n\n void emitSsoEvent('sso.domain.removed', {\n id: config.id,\n tenantId: config.tenantId,\n organizationId: config.organizationId,\n domain: normalized,\n }).catch((e) => console.error('[SSO Event]', e))\n\n return this.toPublic(config)\n }\n\n toPublic(config: SsoConfig): SsoConfigPublic {\n return {\n id: config.id,\n name: config.name ?? null,\n tenantId: config.tenantId ?? null,\n organizationId: config.organizationId,\n protocol: config.protocol,\n issuer: config.issuer ?? null,\n clientId: config.clientId ?? null,\n hasClientSecret: !!config.clientSecretEnc,\n allowedDomains: config.allowedDomains,\n jitEnabled: config.jitEnabled,\n autoLinkByEmail: config.autoLinkByEmail,\n isActive: config.isActive,\n ssoRequired: config.ssoRequired,\n appRoleMappings: config.appRoleMappings ?? {},\n createdAt: config.createdAt,\n updatedAt: config.updatedAt,\n }\n }\n\n private async resolveConfig(scope: SsoAdminScope, id: string): Promise<SsoConfig> {\n const where: FilterQuery<SsoConfig> = { id, deletedAt: null }\n if (!scope.isSuperAdmin) {\n if (!scope.organizationId) throw new SsoConfigError('Organization context is required', 403)\n where.organizationId = scope.organizationId\n }\n\n const config = await this.em.findOne(SsoConfig, where)\n if (!config) throw new SsoConfigError('SSO configuration not found', 404)\n\n return config\n }\n\n private async testConnectionInternal(config: SsoConfig): Promise<{ ok: boolean; error?: string }> {\n const provider = this.ssoProviderRegistry.resolve(config.protocol)\n if (!provider) return { ok: false, error: `No provider for protocol: ${config.protocol}` }\n\n return provider.validateConfig(config)\n }\n}\n\nexport class SsoConfigError extends Error {\n constructor(\n message: string,\n public readonly statusCode: number,\n ) {\n super(message)\n this.name = 'SsoConfigError'\n }\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,125 @@
1
+ import crypto from "node:crypto";
2
+ import { findOneWithDecryption, findWithDecryption } from "@open-mercato/shared/lib/encryption/find";
3
+ import { signJwt } from "@open-mercato/shared/lib/auth/jwt";
4
+ import { SsoConfig } from "../data/entities.js";
5
+ import { encryptStateCookie, decryptStateCookie, createFlowState } from "../lib/state-cookie.js";
6
+ import { emitSsoEvent } from "../events.js";
7
+ class SsoService {
8
+ constructor(em, ssoProviderRegistry, accountLinkingService, tenantEncryptionService, authService, rbacService) {
9
+ this.em = em;
10
+ this.ssoProviderRegistry = ssoProviderRegistry;
11
+ this.accountLinkingService = accountLinkingService;
12
+ this.tenantEncryptionService = tenantEncryptionService;
13
+ this.authService = authService;
14
+ this.rbacService = rbacService;
15
+ }
16
+ async findConfigByEmail(email) {
17
+ const domain = email.split("@")[1]?.toLowerCase();
18
+ if (!domain) return null;
19
+ const configs = await findWithDecryption(
20
+ this.em,
21
+ SsoConfig,
22
+ { isActive: true, deletedAt: null },
23
+ {},
24
+ { tenantId: null }
25
+ );
26
+ return configs.find((c) => c.allowedDomains.some((d) => d.toLowerCase() === domain)) ?? null;
27
+ }
28
+ async initiateLogin(configId, returnUrl, redirectUri) {
29
+ const config = await findOneWithDecryption(
30
+ this.em,
31
+ SsoConfig,
32
+ { id: configId, isActive: true, deletedAt: null },
33
+ {},
34
+ { tenantId: null }
35
+ );
36
+ if (!config) throw new Error("SSO configuration not found or inactive");
37
+ const provider = this.ssoProviderRegistry.resolve(config.protocol);
38
+ if (!provider) throw new Error(`No provider registered for protocol: ${config.protocol}`);
39
+ const clientSecret = await this.decryptClientSecret(config);
40
+ const { state } = createFlowState({ configId, returnUrl });
41
+ void emitSsoEvent("sso.login.initiated", {
42
+ tenantId: config.tenantId,
43
+ organizationId: config.organizationId
44
+ }).catch((e) => console.error("[SSO Event]", e));
45
+ const authUrl = await provider.buildAuthUrl(config, {
46
+ state: state.state,
47
+ nonce: state.nonce,
48
+ redirectUri,
49
+ codeVerifier: state.codeVerifier,
50
+ clientSecret
51
+ });
52
+ const stateCookie = encryptStateCookie(state);
53
+ return { redirectUrl: authUrl, stateCookie };
54
+ }
55
+ async handleOidcCallback(callbackParams, stateCookie, redirectUri) {
56
+ const flowState = decryptStateCookie(stateCookie);
57
+ if (!flowState) throw new Error("Invalid or expired SSO state");
58
+ const receivedState = Buffer.from(callbackParams.state || "");
59
+ const expectedState = Buffer.from(flowState.state);
60
+ if (receivedState.length !== expectedState.length || !crypto.timingSafeEqual(receivedState, expectedState)) {
61
+ throw new Error("State mismatch \u2014 possible CSRF attack");
62
+ }
63
+ const config = await findOneWithDecryption(
64
+ this.em,
65
+ SsoConfig,
66
+ { id: flowState.configId, isActive: true, deletedAt: null },
67
+ {},
68
+ { tenantId: null }
69
+ );
70
+ if (!config) throw new Error("SSO configuration no longer active");
71
+ const provider = this.ssoProviderRegistry.resolve(config.protocol);
72
+ if (!provider) throw new Error(`No provider for protocol: ${config.protocol}`);
73
+ const clientSecret = await this.decryptClientSecret(config);
74
+ const idpPayload = await provider.handleCallback(config, {
75
+ callbackParams,
76
+ redirectUri,
77
+ expectedState: flowState.state,
78
+ expectedNonce: flowState.nonce,
79
+ codeVerifier: flowState.codeVerifier,
80
+ clientSecret
81
+ });
82
+ const tenantId = config.tenantId ?? "";
83
+ const { user } = await this.accountLinkingService.resolveUser(config, idpPayload, tenantId);
84
+ await this.rbacService.invalidateUserCache(String(user.id));
85
+ const roles = await this.authService.getUserRoles(user, tenantId || null);
86
+ const token = signJwt({
87
+ sub: String(user.id),
88
+ tenantId: tenantId || null,
89
+ orgId: user.organizationId ? String(user.organizationId) : null,
90
+ email: user.email,
91
+ roles
92
+ });
93
+ await this.authService.updateLastLoginAt(user);
94
+ const days = Number(process.env.REMEMBER_ME_DAYS || "30");
95
+ const sessionExpiresAt = new Date(Date.now() + days * 24 * 60 * 60 * 1e3);
96
+ const session = await this.authService.createSession(user, sessionExpiresAt);
97
+ void emitSsoEvent("sso.login.completed", {
98
+ id: String(user.id),
99
+ tenantId: config.tenantId,
100
+ organizationId: config.organizationId
101
+ }).catch((e) => console.error("[SSO Event]", e));
102
+ return {
103
+ token,
104
+ sessionToken: session.token,
105
+ sessionExpiresAt,
106
+ redirectUrl: flowState.returnUrl || "/backend",
107
+ tenantId: config.tenantId ?? null,
108
+ organizationId: config.organizationId
109
+ };
110
+ }
111
+ async decryptClientSecret(config) {
112
+ if (!config.clientSecretEnc) return void 0;
113
+ const decrypted = await this.tenantEncryptionService.decryptEntityPayload(
114
+ config.id,
115
+ { clientSecretEnc: config.clientSecretEnc },
116
+ config.tenantId,
117
+ config.organizationId
118
+ );
119
+ return decrypted.clientSecretEnc ?? void 0;
120
+ }
121
+ }
122
+ export {
123
+ SsoService
124
+ };
125
+ //# sourceMappingURL=ssoService.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/sso/services/ssoService.ts"],
4
+ "sourcesContent": ["import crypto from 'node:crypto'\nimport { EntityManager } from '@mikro-orm/postgresql'\nimport { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { signJwt } from '@open-mercato/shared/lib/auth/jwt'\nimport type { AuthService } from '@open-mercato/core/modules/auth/services/authService'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { SsoConfig } from '../data/entities'\nimport type { SsoProviderRegistry } from '../lib/registry'\nimport type { AccountLinkingService } from './accountLinkingService'\nimport { encryptStateCookie, decryptStateCookie, createFlowState } from '../lib/state-cookie'\nimport { emitSsoEvent } from '../events'\nimport type { TenantDataEncryptionService } from '@open-mercato/shared/lib/encryption/tenantDataEncryptionService'\n\nexport class SsoService {\n constructor(\n private em: EntityManager,\n private ssoProviderRegistry: SsoProviderRegistry,\n private accountLinkingService: AccountLinkingService,\n private tenantEncryptionService: TenantDataEncryptionService,\n private authService: AuthService,\n private rbacService: RbacService,\n ) {}\n\n async findConfigByEmail(email: string): Promise<SsoConfig | null> {\n const domain = email.split('@')[1]?.toLowerCase()\n if (!domain) return null\n\n const configs = await findWithDecryption(\n this.em,\n SsoConfig,\n { isActive: true, deletedAt: null },\n {},\n { tenantId: null },\n )\n return configs.find((c) => c.allowedDomains.some((d) => d.toLowerCase() === domain)) ?? null\n }\n\n async initiateLogin(\n configId: string,\n returnUrl: string,\n redirectUri: string,\n ): Promise<{ redirectUrl: string; stateCookie: string }> {\n const config = await findOneWithDecryption(\n this.em,\n SsoConfig,\n { id: configId, isActive: true, deletedAt: null },\n {},\n { tenantId: null },\n )\n if (!config) throw new Error('SSO configuration not found or inactive')\n\n const provider = this.ssoProviderRegistry.resolve(config.protocol)\n if (!provider) throw new Error(`No provider registered for protocol: ${config.protocol}`)\n\n const clientSecret = await this.decryptClientSecret(config)\n\n const { state } = createFlowState({ configId, returnUrl })\n\n void emitSsoEvent('sso.login.initiated', {\n tenantId: config.tenantId,\n organizationId: config.organizationId,\n }).catch((e) => console.error('[SSO Event]', e))\n\n const authUrl = await provider.buildAuthUrl(config, {\n state: state.state,\n nonce: state.nonce,\n redirectUri,\n codeVerifier: state.codeVerifier,\n clientSecret,\n })\n\n const stateCookie = encryptStateCookie(state)\n return { redirectUrl: authUrl, stateCookie }\n }\n\n async handleOidcCallback(\n callbackParams: Record<string, string>,\n stateCookie: string,\n redirectUri: string,\n ): Promise<{\n token: string\n sessionToken: string\n sessionExpiresAt: Date\n redirectUrl: string\n tenantId: string | null\n organizationId: string\n }> {\n const flowState = decryptStateCookie(stateCookie)\n if (!flowState) throw new Error('Invalid or expired SSO state')\n\n const receivedState = Buffer.from(callbackParams.state || '')\n const expectedState = Buffer.from(flowState.state)\n if (receivedState.length !== expectedState.length || !crypto.timingSafeEqual(receivedState, expectedState)) {\n throw new Error('State mismatch \u2014 possible CSRF attack')\n }\n\n const config = await findOneWithDecryption(\n this.em,\n SsoConfig,\n { id: flowState.configId, isActive: true, deletedAt: null },\n {},\n { tenantId: null },\n )\n if (!config) throw new Error('SSO configuration no longer active')\n\n const provider = this.ssoProviderRegistry.resolve(config.protocol)\n if (!provider) throw new Error(`No provider for protocol: ${config.protocol}`)\n\n const clientSecret = await this.decryptClientSecret(config)\n\n const idpPayload = await provider.handleCallback(config, {\n callbackParams,\n redirectUri,\n expectedState: flowState.state,\n expectedNonce: flowState.nonce,\n codeVerifier: flowState.codeVerifier,\n clientSecret,\n })\n\n const tenantId = config.tenantId ?? ''\n const { user } = await this.accountLinkingService.resolveUser(config, idpPayload, tenantId)\n\n await this.rbacService.invalidateUserCache(String(user.id))\n\n const roles = await this.authService.getUserRoles(user, tenantId || null)\n const token = signJwt({\n sub: String(user.id),\n tenantId: tenantId || null,\n orgId: user.organizationId ? String(user.organizationId) : null,\n email: user.email,\n roles,\n })\n\n await this.authService.updateLastLoginAt(user)\n\n const days = Number(process.env.REMEMBER_ME_DAYS || '30')\n const sessionExpiresAt = new Date(Date.now() + days * 24 * 60 * 60 * 1000)\n const session = await this.authService.createSession(user, sessionExpiresAt)\n\n void emitSsoEvent('sso.login.completed', {\n id: String(user.id),\n tenantId: config.tenantId,\n organizationId: config.organizationId,\n }).catch((e) => console.error('[SSO Event]', e))\n\n return {\n token,\n sessionToken: session.token,\n sessionExpiresAt,\n redirectUrl: flowState.returnUrl || '/backend',\n tenantId: config.tenantId ?? null,\n organizationId: config.organizationId,\n }\n }\n\n private async decryptClientSecret(config: SsoConfig): Promise<string | undefined> {\n if (!config.clientSecretEnc) return undefined\n\n const decrypted = await this.tenantEncryptionService.decryptEntityPayload(\n config.id,\n { clientSecretEnc: config.clientSecretEnc },\n config.tenantId,\n config.organizationId,\n )\n return (decrypted.clientSecretEnc as string) ?? undefined\n }\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,47 @@
1
+ import { SsoConfig } from "./data/entities.js";
2
+ const setup = {
3
+ defaultRoleFeatures: {
4
+ superadmin: ["sso.*"],
5
+ admin: ["sso.config.view", "sso.config.manage", "sso.scim.manage"]
6
+ },
7
+ async seedDefaults({ em, tenantId, organizationId, container }) {
8
+ if (process.env.NODE_ENV !== "development") return;
9
+ if (process.env.SSO_DEV_SEED !== "true") return;
10
+ const clientSecret = process.env.SSO_DEV_CLIENT_SECRET;
11
+ if (!clientSecret) return;
12
+ const domains = (process.env.SSO_DEV_ALLOWED_DOMAINS || "example.com").split(",").map((d) => d.trim()).filter(Boolean);
13
+ const existing = await em.findOne(SsoConfig, { organizationId });
14
+ if (existing) {
15
+ existing.allowedDomains = domains;
16
+ await em.flush();
17
+ return;
18
+ }
19
+ const encryptionService = container.resolve("tenantEncryptionService");
20
+ const encrypted = await encryptionService.encryptEntityPayload(
21
+ "SsoConfig",
22
+ { clientSecretEnc: clientSecret },
23
+ tenantId,
24
+ organizationId
25
+ );
26
+ const config = em.create(SsoConfig, {
27
+ tenantId,
28
+ organizationId,
29
+ protocol: "oidc",
30
+ issuer: process.env.SSO_DEV_ISSUER || "http://localhost:8080/realms/open-mercato",
31
+ clientId: process.env.SSO_DEV_CLIENT_ID || "open-mercato-app",
32
+ clientSecretEnc: encrypted.clientSecretEnc,
33
+ allowedDomains: domains,
34
+ jitEnabled: true,
35
+ autoLinkByEmail: true,
36
+ isActive: true,
37
+ ssoRequired: false
38
+ });
39
+ await em.persistAndFlush(config);
40
+ }
41
+ };
42
+ var setup_default = setup;
43
+ export {
44
+ setup_default as default,
45
+ setup
46
+ };
47
+ //# sourceMappingURL=setup.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/modules/sso/setup.ts"],
4
+ "sourcesContent": ["import type { RequiredEntityData } from '@mikro-orm/core'\nimport type { ModuleSetupConfig } from '@open-mercato/shared/modules/setup'\nimport type { TenantDataEncryptionService } from '@open-mercato/shared/lib/encryption/tenantDataEncryptionService'\nimport { SsoConfig } from './data/entities'\n\nexport const setup: ModuleSetupConfig = {\n defaultRoleFeatures: {\n superadmin: ['sso.*'],\n admin: ['sso.config.view', 'sso.config.manage', 'sso.scim.manage'],\n },\n\n async seedDefaults({ em, tenantId, organizationId, container }) {\n if (process.env.NODE_ENV !== 'development') return\n if (process.env.SSO_DEV_SEED !== 'true') return\n\n const clientSecret = process.env.SSO_DEV_CLIENT_SECRET\n if (!clientSecret) return\n\n const domains = (process.env.SSO_DEV_ALLOWED_DOMAINS || 'example.com')\n .split(',')\n .map((d) => d.trim())\n .filter(Boolean)\n\n const existing = await em.findOne(SsoConfig, { organizationId })\n if (existing) {\n existing.allowedDomains = domains\n await em.flush()\n return\n }\n\n const encryptionService = container.resolve<TenantDataEncryptionService>('tenantEncryptionService')\n const encrypted = await encryptionService.encryptEntityPayload(\n 'SsoConfig',\n { clientSecretEnc: clientSecret },\n tenantId,\n organizationId,\n )\n\n const config = em.create(SsoConfig, {\n tenantId,\n organizationId,\n protocol: 'oidc',\n issuer: process.env.SSO_DEV_ISSUER || 'http://localhost:8080/realms/open-mercato',\n clientId: process.env.SSO_DEV_CLIENT_ID || 'open-mercato-app',\n clientSecretEnc: encrypted.clientSecretEnc as string,\n allowedDomains: domains,\n jitEnabled: true,\n autoLinkByEmail: true,\n isActive: true,\n ssoRequired: false,\n } as RequiredEntityData<SsoConfig>)\n await em.persistAndFlush(config)\n },\n}\n\nexport default setup\n"],
5
+ "mappings": "AAGA,SAAS,iBAAiB;AAEnB,MAAM,QAA2B;AAAA,EACtC,qBAAqB;AAAA,IACnB,YAAY,CAAC,OAAO;AAAA,IACpB,OAAO,CAAC,mBAAmB,qBAAqB,iBAAiB;AAAA,EACnE;AAAA,EAEA,MAAM,aAAa,EAAE,IAAI,UAAU,gBAAgB,UAAU,GAAG;AAC9D,QAAI,QAAQ,IAAI,aAAa,cAAe;AAC5C,QAAI,QAAQ,IAAI,iBAAiB,OAAQ;AAEzC,UAAM,eAAe,QAAQ,IAAI;AACjC,QAAI,CAAC,aAAc;AAEnB,UAAM,WAAW,QAAQ,IAAI,2BAA2B,eACrD,MAAM,GAAG,EACT,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,EACnB,OAAO,OAAO;AAEjB,UAAM,WAAW,MAAM,GAAG,QAAQ,WAAW,EAAE,eAAe,CAAC;AAC/D,QAAI,UAAU;AACZ,eAAS,iBAAiB;AAC1B,YAAM,GAAG,MAAM;AACf;AAAA,IACF;AAEA,UAAM,oBAAoB,UAAU,QAAqC,yBAAyB;AAClG,UAAM,YAAY,MAAM,kBAAkB;AAAA,MACxC;AAAA,MACA,EAAE,iBAAiB,aAAa;AAAA,MAChC;AAAA,MACA;AAAA,IACF;AAEA,UAAM,SAAS,GAAG,OAAO,WAAW;AAAA,MAClC;AAAA,MACA;AAAA,MACA,UAAU;AAAA,MACV,QAAQ,QAAQ,IAAI,kBAAkB;AAAA,MACtC,UAAU,QAAQ,IAAI,qBAAqB;AAAA,MAC3C,iBAAiB,UAAU;AAAA,MAC3B,gBAAgB;AAAA,MAChB,YAAY;AAAA,MACZ,iBAAiB;AAAA,MACjB,UAAU;AAAA,MACV,aAAa;AAAA,IACf,CAAkC;AAClC,UAAM,GAAG,gBAAgB,MAAM;AAAA,EACjC;AACF;AAEA,IAAO,gBAAQ;",
6
+ "names": []
7
+ }
@@ -0,0 +1,21 @@
1
+ import { SsoIdentity, SsoRoleGrant, SsoUserDeactivation } from "../data/entities.js";
2
+ const metadata = {
3
+ event: "auth.user.deleted",
4
+ persistent: true,
5
+ id: "sso:user-deleted-cleanup"
6
+ };
7
+ async function handle(payload, ctx) {
8
+ const em = ctx.resolve("em");
9
+ await em.nativeUpdate(
10
+ SsoIdentity,
11
+ { userId: payload.userId, deletedAt: null },
12
+ { deletedAt: /* @__PURE__ */ new Date() }
13
+ );
14
+ await em.nativeDelete(SsoRoleGrant, { userId: payload.userId });
15
+ await em.nativeDelete(SsoUserDeactivation, { userId: payload.userId });
16
+ }
17
+ export {
18
+ handle as default,
19
+ metadata
20
+ };
21
+ //# sourceMappingURL=user-deleted-cleanup.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/sso/subscribers/user-deleted-cleanup.ts"],
4
+ "sourcesContent": ["import type { EntityData } from '@mikro-orm/core'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { SsoIdentity, SsoRoleGrant, SsoUserDeactivation } from '../data/entities'\n\nexport const metadata = {\n event: 'auth.user.deleted',\n persistent: true,\n id: 'sso:user-deleted-cleanup',\n}\n\ntype UserDeletedPayload = {\n userId: string\n tenantId: string\n organizationId: string\n}\n\ntype ResolverContext = {\n resolve: <T = unknown>(name: string) => T\n}\n\nexport default async function handle(payload: UserDeletedPayload, ctx: ResolverContext) {\n const em = ctx.resolve<EntityManager>('em')\n\n await em.nativeUpdate(\n SsoIdentity,\n { userId: payload.userId, deletedAt: null },\n { deletedAt: new Date() } as EntityData<SsoIdentity>,\n )\n\n await em.nativeDelete(SsoRoleGrant, { userId: payload.userId })\n\n await em.nativeDelete(SsoUserDeactivation, { userId: payload.userId })\n}\n"],
5
+ "mappings": "AAEA,SAAS,aAAa,cAAc,2BAA2B;AAExD,MAAM,WAAW;AAAA,EACtB,OAAO;AAAA,EACP,YAAY;AAAA,EACZ,IAAI;AACN;AAYA,eAAO,OAA8B,SAA6B,KAAsB;AACtF,QAAM,KAAK,IAAI,QAAuB,IAAI;AAE1C,QAAM,GAAG;AAAA,IACP;AAAA,IACA,EAAE,QAAQ,QAAQ,QAAQ,WAAW,KAAK;AAAA,IAC1C,EAAE,WAAW,oBAAI,KAAK,EAAE;AAAA,EAC1B;AAEA,QAAM,GAAG,aAAa,cAAc,EAAE,QAAQ,QAAQ,OAAO,CAAC;AAE9D,QAAM,GAAG,aAAa,qBAAqB,EAAE,QAAQ,QAAQ,OAAO,CAAC;AACvE;",
6
+ "names": []
7
+ }
@@ -0,0 +1,106 @@
1
+ "use client";
2
+ import { jsx } from "react/jsx-runtime";
3
+ import { useCallback, useEffect, useRef, useState } from "react";
4
+ import { apiCall } from "@open-mercato/ui/backend/utils/apiCall";
5
+ import { useT } from "@open-mercato/shared/lib/i18n/context";
6
+ import { translateWithFallback } from "@open-mercato/shared/lib/i18n/translate";
7
+ const SSO_ERROR_CODES = [
8
+ "sso_failed",
9
+ "sso_missing_config",
10
+ "sso_email_not_verified",
11
+ "sso_state_missing",
12
+ "sso_idp_error",
13
+ "sso_missing_params"
14
+ ];
15
+ const HRD_DEBOUNCE_MS = 300;
16
+ function SsoLoginWidget({ context }) {
17
+ const t = useT();
18
+ const translate = useCallback(
19
+ (key, fallback) => translateWithFallback(t, key, fallback),
20
+ [t]
21
+ );
22
+ const [ssoActive, setSsoActive] = useState(false);
23
+ const lastCheckedEmail = useRef("");
24
+ const debounceTimer = useRef(null);
25
+ useEffect(() => {
26
+ const errorParam = context.searchParams.get("error");
27
+ if (!errorParam) return;
28
+ const errorMessages = {
29
+ sso_failed: translate("sso.login.errors.failed", "SSO login failed. Please try again."),
30
+ sso_missing_config: translate("sso.login.errors.missingConfig", "SSO is not configured for this account."),
31
+ sso_email_not_verified: translate("sso.login.errors.emailNotVerified", "Your email address is not verified by the identity provider. Please verify your email and try again."),
32
+ sso_state_missing: translate("sso.login.errors.stateMissing", "SSO session expired. Please try again."),
33
+ sso_idp_error: translate("sso.login.errors.idpError", "The identity provider returned an error. Please try again or contact your administrator."),
34
+ sso_missing_params: translate("sso.login.errors.missingParams", "SSO callback was incomplete. Please try again.")
35
+ };
36
+ if (SSO_ERROR_CODES.includes(errorParam)) {
37
+ context.setError(errorMessages[errorParam] ?? errorMessages.sso_failed);
38
+ }
39
+ }, [context.searchParams, context.setError, translate]);
40
+ const checkHrd = useCallback(async (email) => {
41
+ if (!email || !email.includes("@")) {
42
+ context.setAuthOverride(null);
43
+ setSsoActive(false);
44
+ lastCheckedEmail.current = "";
45
+ return;
46
+ }
47
+ if (email === lastCheckedEmail.current) return;
48
+ lastCheckedEmail.current = email;
49
+ try {
50
+ const body = { email };
51
+ if (context.tenantId) {
52
+ body.tenantId = context.tenantId;
53
+ }
54
+ const res = await apiCall(
55
+ "/api/sso/hrd",
56
+ { method: "POST", body: JSON.stringify(body), headers: { "Content-Type": "application/json" } }
57
+ );
58
+ if (res.result?.hasSso && res.result.configId) {
59
+ const configId = res.result.configId;
60
+ setSsoActive(true);
61
+ context.setAuthOverride({
62
+ providerId: "sso",
63
+ providerLabel: translate("sso.login.continueWithSso", "Continue with SSO"),
64
+ onSubmit: () => {
65
+ const returnUrl = context.searchParams.get("returnUrl") || "/backend";
66
+ window.location.href = `/api/sso/initiate?configId=${encodeURIComponent(configId)}&returnUrl=${encodeURIComponent(returnUrl)}`;
67
+ },
68
+ hidePassword: true,
69
+ hideRememberMe: true,
70
+ hideForgotPassword: true
71
+ });
72
+ } else {
73
+ setSsoActive(false);
74
+ context.setAuthOverride(null);
75
+ }
76
+ } catch {
77
+ setSsoActive(false);
78
+ context.setAuthOverride(null);
79
+ }
80
+ }, [context, translate]);
81
+ useEffect(() => {
82
+ if (debounceTimer.current) {
83
+ clearTimeout(debounceTimer.current);
84
+ }
85
+ if (!context.email) {
86
+ setSsoActive(false);
87
+ context.setAuthOverride(null);
88
+ lastCheckedEmail.current = "";
89
+ return;
90
+ }
91
+ debounceTimer.current = setTimeout(() => {
92
+ checkHrd(context.email);
93
+ }, HRD_DEBOUNCE_MS);
94
+ return () => {
95
+ if (debounceTimer.current) {
96
+ clearTimeout(debounceTimer.current);
97
+ }
98
+ };
99
+ }, [context.email, checkHrd]);
100
+ if (!ssoActive) return null;
101
+ return /* @__PURE__ */ jsx("div", { className: "rounded-md border border-blue-200 bg-blue-50 px-3 py-2 text-center text-xs text-blue-800", children: translate("sso.login.ssoEnabled", "SSO is enabled for this account") });
102
+ }
103
+ export {
104
+ SsoLoginWidget as default
105
+ };
106
+ //# sourceMappingURL=widget.client.js.map