@open-mercato/enterprise 0.5.1-develop.2691.d8a0934b37 → 0.5.1-develop.2694.732417c5ec
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.
- package/dist/modules/record_locks/data/entities.js +2 -1
- package/dist/modules/record_locks/data/entities.js.map +2 -2
- package/dist/modules/record_locks/lib/recordLockService.js +19 -15
- package/dist/modules/record_locks/lib/recordLockService.js.map +2 -2
- package/dist/modules/security/data/entities.js +1 -1
- package/dist/modules/security/data/entities.js.map +1 -1
- package/dist/modules/sso/data/entities.js +1 -1
- package/dist/modules/sso/data/entities.js.map +2 -2
- package/dist/modules/sso/services/accountLinkingService.js +4 -4
- package/dist/modules/sso/services/accountLinkingService.js.map +2 -2
- package/dist/modules/sso/services/hrdService.js +3 -2
- package/dist/modules/sso/services/hrdService.js.map +2 -2
- package/dist/modules/sso/services/scimService.js +7 -7
- package/dist/modules/sso/services/scimService.js.map +2 -2
- package/dist/modules/sso/services/scimTokenService.js +1 -1
- package/dist/modules/sso/services/scimTokenService.js.map +2 -2
- package/dist/modules/sso/services/ssoConfigService.js +1 -1
- package/dist/modules/sso/services/ssoConfigService.js.map +2 -2
- package/dist/modules/sso/setup.js +1 -1
- package/dist/modules/sso/setup.js.map +2 -2
- package/jest.config.cjs +4 -2
- package/package.json +5 -5
- package/src/modules/record_locks/data/entities.ts +2 -1
- package/src/modules/record_locks/lib/recordLockService.ts +33 -28
- package/src/modules/security/data/entities.ts +1 -1
- package/src/modules/sso/data/entities.ts +1 -1
- package/src/modules/sso/services/accountLinkingService.ts +4 -4
- package/src/modules/sso/services/hrdService.ts +10 -7
- package/src/modules/sso/services/scimService.ts +7 -7
- package/src/modules/sso/services/scimTokenService.ts +1 -1
- package/src/modules/sso/services/ssoConfigService.ts +1 -1
- package/src/modules/sso/setup.ts +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
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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
|
|
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.persist(config).flush()\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
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
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.
|
|
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,
|
|
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.persist(config).flush()\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,QAAQ,MAAM,EAAE,MAAM;AAAA,EACjC;AACF;AAEA,IAAO,gBAAQ;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/jest.config.cjs
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
/** @type {import('jest').Config} */
|
|
2
2
|
module.exports = {
|
|
3
|
-
preset: 'ts-jest',
|
|
4
3
|
testEnvironment: 'node',
|
|
5
4
|
watchman: false,
|
|
6
5
|
rootDir: '.',
|
|
@@ -14,7 +13,7 @@ module.exports = {
|
|
|
14
13
|
},
|
|
15
14
|
transform: {
|
|
16
15
|
'^.+\\.(t|j)sx?$': [
|
|
17
|
-
'
|
|
16
|
+
'<rootDir>/../../scripts/jest-mikroorm-transformer.cjs',
|
|
18
17
|
{
|
|
19
18
|
tsconfig: {
|
|
20
19
|
jsx: 'react-jsx',
|
|
@@ -22,6 +21,9 @@ module.exports = {
|
|
|
22
21
|
},
|
|
23
22
|
],
|
|
24
23
|
},
|
|
24
|
+
transformIgnorePatterns: [
|
|
25
|
+
'node_modules/(?!(@mikro-orm)/)',
|
|
26
|
+
],
|
|
25
27
|
testMatch: ['<rootDir>/src/**/__tests__/**/*.test.(ts|tsx)'],
|
|
26
28
|
passWithNoTests: true,
|
|
27
29
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/enterprise",
|
|
3
|
-
"version": "0.5.1-develop.
|
|
3
|
+
"version": "0.5.1-develop.2694.732417c5ec",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -64,8 +64,8 @@
|
|
|
64
64
|
}
|
|
65
65
|
},
|
|
66
66
|
"dependencies": {
|
|
67
|
-
"@open-mercato/core": "0.5.1-develop.
|
|
68
|
-
"@open-mercato/ui": "0.5.1-develop.
|
|
67
|
+
"@open-mercato/core": "0.5.1-develop.2694.732417c5ec",
|
|
68
|
+
"@open-mercato/ui": "0.5.1-develop.2694.732417c5ec",
|
|
69
69
|
"@simplewebauthn/browser": "^13.3.0",
|
|
70
70
|
"@simplewebauthn/server": "^13.3.0",
|
|
71
71
|
"@simplewebauthn/types": "^12.0.0",
|
|
@@ -75,10 +75,10 @@
|
|
|
75
75
|
"qrcode": "^1.5.4"
|
|
76
76
|
},
|
|
77
77
|
"peerDependencies": {
|
|
78
|
-
"@open-mercato/shared": "0.5.1-develop.
|
|
78
|
+
"@open-mercato/shared": "0.5.1-develop.2694.732417c5ec"
|
|
79
79
|
},
|
|
80
80
|
"devDependencies": {
|
|
81
|
-
"@open-mercato/shared": "0.5.1-develop.
|
|
81
|
+
"@open-mercato/shared": "0.5.1-develop.2694.732417c5ec",
|
|
82
82
|
"@types/jest": "^30.0.0",
|
|
83
83
|
"jest": "^30.3.0",
|
|
84
84
|
"ts-jest": "^29.4.9"
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { OptionalProps } from '@mikro-orm/core'
|
|
2
|
+
import { Entity, Index, PrimaryKey, Property, Unique } from '@mikro-orm/decorators/legacy'
|
|
2
3
|
|
|
3
4
|
export type RecordLockStatus = 'active' | 'released' | 'expired' | 'force_released'
|
|
4
5
|
export type RecordLockStrategy = 'optimistic' | 'pessimistic'
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from 'crypto'
|
|
2
2
|
import { UniqueConstraintViolationException, type FilterQuery } from '@mikro-orm/core'
|
|
3
3
|
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
4
|
-
import type
|
|
4
|
+
import { type Kysely, sql } from 'kysely'
|
|
5
5
|
import type { ModuleConfigService } from '@open-mercato/core/modules/configs/lib/module-config-service'
|
|
6
6
|
import { ActionLog } from '@open-mercato/core/modules/audit_logs/data/entities'
|
|
7
7
|
import type { ActionLogService } from '@open-mercato/core/modules/audit_logs/services/actionLogService'
|
|
@@ -268,8 +268,8 @@ function isActiveLockScopeUniqueViolation(error: unknown): boolean {
|
|
|
268
268
|
return false
|
|
269
269
|
}
|
|
270
270
|
|
|
271
|
-
function
|
|
272
|
-
return (em
|
|
271
|
+
function getKysely(em: EntityManager): Kysely<any> {
|
|
272
|
+
return (em as unknown as { getKysely: () => Kysely<any> }).getKysely()
|
|
273
273
|
}
|
|
274
274
|
|
|
275
275
|
const SKIPPED_CONFLICT_FIELDS = new Set([
|
|
@@ -1260,39 +1260,44 @@ export class RecordLockService {
|
|
|
1260
1260
|
|
|
1261
1261
|
private async cleanupHistoricalRecords(tenantId: string): Promise<void> {
|
|
1262
1262
|
try {
|
|
1263
|
-
const
|
|
1263
|
+
const db = getKysely(this.em)
|
|
1264
1264
|
const now = Date.now()
|
|
1265
1265
|
const lockCutoff = new Date(now - LOCK_RETENTION_MS)
|
|
1266
1266
|
const resolvedConflictCutoff = new Date(now - RESOLVED_CONFLICT_RETENTION_MS)
|
|
1267
1267
|
const pendingConflictCutoff = new Date(now - PENDING_CONFLICT_RETENTION_MS)
|
|
1268
1268
|
const deletedAt = new Date(now)
|
|
1269
1269
|
|
|
1270
|
-
await
|
|
1271
|
-
.
|
|
1272
|
-
.
|
|
1273
|
-
.whereNot('status', ACTIVE_LOCK_STATUS)
|
|
1274
|
-
.andWhere('updated_at', '<', lockCutoff)
|
|
1275
|
-
.update({
|
|
1270
|
+
await db
|
|
1271
|
+
.updateTable('record_locks' as any)
|
|
1272
|
+
.set({
|
|
1276
1273
|
deleted_at: deletedAt,
|
|
1277
1274
|
updated_at: deletedAt,
|
|
1278
|
-
})
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
.where(
|
|
1282
|
-
.
|
|
1283
|
-
.
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
.orWhere((resolved) => {
|
|
1289
|
-
resolved.whereNot('status', 'pending').andWhere('updated_at', '<', resolvedConflictCutoff)
|
|
1290
|
-
})
|
|
1291
|
-
})
|
|
1292
|
-
.update({
|
|
1275
|
+
} as any)
|
|
1276
|
+
.where('tenant_id' as any, '=', tenantId)
|
|
1277
|
+
.where('deleted_at' as any, 'is', null as any)
|
|
1278
|
+
.where('status' as any, '!=', ACTIVE_LOCK_STATUS)
|
|
1279
|
+
.where('updated_at' as any, '<', lockCutoff)
|
|
1280
|
+
.execute()
|
|
1281
|
+
|
|
1282
|
+
await db
|
|
1283
|
+
.updateTable('record_lock_conflicts' as any)
|
|
1284
|
+
.set({
|
|
1293
1285
|
deleted_at: deletedAt,
|
|
1294
1286
|
updated_at: deletedAt,
|
|
1295
|
-
})
|
|
1287
|
+
} as any)
|
|
1288
|
+
.where('tenant_id' as any, '=', tenantId)
|
|
1289
|
+
.where('deleted_at' as any, 'is', null as any)
|
|
1290
|
+
.where((eb: any) => eb.or([
|
|
1291
|
+
eb.and([
|
|
1292
|
+
eb('status' as any, '=', 'pending'),
|
|
1293
|
+
eb('created_at' as any, '<', pendingConflictCutoff),
|
|
1294
|
+
]),
|
|
1295
|
+
eb.and([
|
|
1296
|
+
eb('status' as any, '!=', 'pending'),
|
|
1297
|
+
eb('updated_at' as any, '<', resolvedConflictCutoff),
|
|
1298
|
+
]),
|
|
1299
|
+
]))
|
|
1300
|
+
.execute()
|
|
1296
1301
|
} catch {
|
|
1297
1302
|
// Best-effort cleanup must never fail lock workflows.
|
|
1298
1303
|
}
|
|
@@ -1644,8 +1649,8 @@ export class RecordLockService {
|
|
|
1644
1649
|
|
|
1645
1650
|
const result = await this.em.transactional(async (tx) => {
|
|
1646
1651
|
try {
|
|
1647
|
-
const
|
|
1648
|
-
await
|
|
1652
|
+
const db = getKysely(tx as EntityManager)
|
|
1653
|
+
await sql`select pg_advisory_xact_lock(hashtext(${dedupeKey}))`.execute(db)
|
|
1649
1654
|
} catch {
|
|
1650
1655
|
// Best-effort lock; fallback to find-first behavior below.
|
|
1651
1656
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Entity, PrimaryKey, Property, Unique
|
|
1
|
+
import { Entity, Index, PrimaryKey, Property, Unique } from '@mikro-orm/decorators/legacy'
|
|
2
2
|
|
|
3
3
|
@Entity({ tableName: 'sso_configs' })
|
|
4
4
|
// Unique index on organization_id (partial: WHERE deleted_at IS NULL) — managed by migration
|
|
@@ -120,7 +120,7 @@ export class AccountLinkingService {
|
|
|
120
120
|
createdAt: now,
|
|
121
121
|
updatedAt: now,
|
|
122
122
|
} as RequiredEntityData<SsoIdentity>)
|
|
123
|
-
await this.em.
|
|
123
|
+
await this.em.persist(identity).flush()
|
|
124
124
|
|
|
125
125
|
void emitSsoEvent('sso.identity.linked', {
|
|
126
126
|
id: identity.id,
|
|
@@ -147,7 +147,7 @@ export class AccountLinkingService {
|
|
|
147
147
|
isConfirmed: true,
|
|
148
148
|
createdAt: new Date(),
|
|
149
149
|
})
|
|
150
|
-
await txEm.
|
|
150
|
+
await txEm.persist(user).flush()
|
|
151
151
|
|
|
152
152
|
await this.assignRolesFromSso(txEm, user, config, tenantId, idpPayload.groups)
|
|
153
153
|
|
|
@@ -167,7 +167,7 @@ export class AccountLinkingService {
|
|
|
167
167
|
createdAt: now,
|
|
168
168
|
updatedAt: now,
|
|
169
169
|
} as RequiredEntityData<SsoIdentity>)
|
|
170
|
-
await txEm.
|
|
170
|
+
await txEm.persist(identity).flush()
|
|
171
171
|
|
|
172
172
|
void emitSsoEvent('sso.identity.created', {
|
|
173
173
|
id: identity.id,
|
|
@@ -290,7 +290,7 @@ export class AccountLinkingService {
|
|
|
290
290
|
if (existingLink) return
|
|
291
291
|
|
|
292
292
|
const userRole = em.create(UserRole, { user, role, createdAt: new Date() })
|
|
293
|
-
await em.
|
|
293
|
+
await em.persist(userRole).flush()
|
|
294
294
|
}
|
|
295
295
|
}
|
|
296
296
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { EntityManager } from '@mikro-orm/postgresql'
|
|
2
|
+
import { type Kysely, sql } from 'kysely'
|
|
2
3
|
import { SsoConfig } from '../data/entities'
|
|
3
4
|
|
|
4
5
|
export class HrdService {
|
|
@@ -8,15 +9,17 @@ export class HrdService {
|
|
|
8
9
|
const domain = email.split('@')[1]?.toLowerCase()
|
|
9
10
|
if (!domain) return null
|
|
10
11
|
|
|
11
|
-
const
|
|
12
|
-
const row = await
|
|
13
|
-
.
|
|
14
|
-
.
|
|
15
|
-
.
|
|
16
|
-
.
|
|
12
|
+
const db = (this.em as any).getKysely() as Kysely<any>
|
|
13
|
+
const row = await db
|
|
14
|
+
.selectFrom('sso_configs' as any)
|
|
15
|
+
.selectAll()
|
|
16
|
+
.where(sql<boolean>`allowed_domains @> ${JSON.stringify([domain])}::jsonb`)
|
|
17
|
+
.where('is_active' as any, '=', true)
|
|
18
|
+
.where('deleted_at' as any, 'is', null as any)
|
|
19
|
+
.executeTakeFirst()
|
|
17
20
|
|
|
18
21
|
if (!row) return null
|
|
19
22
|
|
|
20
|
-
return this.em.map(SsoConfig, row)
|
|
23
|
+
return this.em.map(SsoConfig, row as Record<string, unknown>)
|
|
21
24
|
}
|
|
22
25
|
}
|
|
@@ -92,7 +92,7 @@ export class ScimService {
|
|
|
92
92
|
createdAt: now,
|
|
93
93
|
updatedAt: now,
|
|
94
94
|
} as RequiredEntityData<SsoIdentity>)
|
|
95
|
-
await this.em.
|
|
95
|
+
await this.em.persist(identity).flush()
|
|
96
96
|
|
|
97
97
|
const deactivation = parsed.active === false
|
|
98
98
|
? await this.createDeactivation(existingUser.id, scope)
|
|
@@ -117,7 +117,7 @@ export class ScimService {
|
|
|
117
117
|
isConfirmed: true,
|
|
118
118
|
createdAt: new Date(),
|
|
119
119
|
})
|
|
120
|
-
await txEm.
|
|
120
|
+
await txEm.persist(user).flush()
|
|
121
121
|
|
|
122
122
|
const now = new Date()
|
|
123
123
|
const identity = txEm.create(SsoIdentity, {
|
|
@@ -134,7 +134,7 @@ export class ScimService {
|
|
|
134
134
|
createdAt: now,
|
|
135
135
|
updatedAt: now,
|
|
136
136
|
} as RequiredEntityData<SsoIdentity>)
|
|
137
|
-
await txEm.
|
|
137
|
+
await txEm.persist(identity).flush()
|
|
138
138
|
|
|
139
139
|
const deactivation = parsed.active === false
|
|
140
140
|
? await this.createDeactivationTx(txEm, user.id, scope)
|
|
@@ -384,7 +384,7 @@ export class ScimService {
|
|
|
384
384
|
ssoConfigId: scope.ssoConfigId,
|
|
385
385
|
deactivatedAt: new Date(),
|
|
386
386
|
} as RequiredEntityData<SsoUserDeactivation>)
|
|
387
|
-
await this.em.
|
|
387
|
+
await this.em.persist(deactivation).flush()
|
|
388
388
|
return deactivation
|
|
389
389
|
}
|
|
390
390
|
|
|
@@ -396,7 +396,7 @@ export class ScimService {
|
|
|
396
396
|
ssoConfigId: scope.ssoConfigId,
|
|
397
397
|
deactivatedAt: new Date(),
|
|
398
398
|
} as RequiredEntityData<SsoUserDeactivation>)
|
|
399
|
-
await txEm.
|
|
399
|
+
await txEm.persist(deactivation).flush()
|
|
400
400
|
return deactivation
|
|
401
401
|
}
|
|
402
402
|
|
|
@@ -419,7 +419,7 @@ export class ScimService {
|
|
|
419
419
|
responseStatus,
|
|
420
420
|
errorMessage: errorMessage ?? null,
|
|
421
421
|
} as RequiredEntityData<ScimProvisioningLog>)
|
|
422
|
-
await this.em.
|
|
422
|
+
await this.em.persist(entry).flush()
|
|
423
423
|
}
|
|
424
424
|
|
|
425
425
|
private async logTx(
|
|
@@ -440,7 +440,7 @@ export class ScimService {
|
|
|
440
440
|
scimExternalId: externalId ?? null,
|
|
441
441
|
responseStatus,
|
|
442
442
|
} as RequiredEntityData<ScimProvisioningLog>)
|
|
443
|
-
await txEm.
|
|
443
|
+
await txEm.persist(entry).flush()
|
|
444
444
|
}
|
|
445
445
|
}
|
|
446
446
|
|
|
@@ -58,7 +58,7 @@ export class ScimTokenService {
|
|
|
58
58
|
organizationId: scope.organizationId!,
|
|
59
59
|
} as RequiredEntityData<ScimToken>)
|
|
60
60
|
|
|
61
|
-
await this.em.
|
|
61
|
+
await this.em.persist(token).flush()
|
|
62
62
|
|
|
63
63
|
return { id: token.id, token: raw, prefix: tokenPrefix, name }
|
|
64
64
|
}
|
|
@@ -132,7 +132,7 @@ export class SsoConfigService {
|
|
|
132
132
|
appRoleMappings: input.appRoleMappings ?? {},
|
|
133
133
|
} as RequiredEntityData<SsoConfig>)
|
|
134
134
|
|
|
135
|
-
await this.em.
|
|
135
|
+
await this.em.persist(config).flush()
|
|
136
136
|
|
|
137
137
|
void emitSsoEvent('sso.config.created', {
|
|
138
138
|
id: config.id,
|