import { Injectable, Logger } from '@nestjs/common'; import { Cron, CronExpression } from '@nestjs/schedule'; import { LdapConfigService } from './ldap-config.service'; import { LdapService } from './ldap.service'; /** * LDAP Sync Scheduler (D-14 auto-sync). * * Runs every minute and checks each active LDAP config to determine * if a sync is due based on syncIntervalMin. * * CRITICAL per Pitfall 2: Each tenant sync is an independent operation * with its own tenant context. We do NOT share database connections * across tenant syncs. */ @Injectable() export class LdapSyncScheduler { private readonly logger = new Logger(LdapSyncScheduler.name); constructor( private ldapService: LdapService, private ldapConfigService: LdapConfigService, ) {} /** * Cron job running every minute. Checks all active LDAP configs * and triggers sync for those whose interval has elapsed. */ @Cron(CronExpression.EVERY_MINUTE) async handleCron() { const configs = await this.ldapConfigService.getAllActiveConfigs(); for (const config of configs) { try { // Skip configs with auto-sync disabled (interval = 0) if (config.syncIntervalMin <= 0) { continue; } // Check if sync is due const now = new Date(); if (config.lastSyncAt) { const elapsed = (now.getTime() - config.lastSyncAt.getTime()) / 1000 / 60; if (elapsed < config.syncIntervalMin) { continue; // Not yet time for next sync } } // If lastSyncAt is null, sync has never run -- trigger now this.logger.log( `Starting LDAP sync for tenant ${config.tenantId} (interval: ${config.syncIntervalMin}min)`, ); // CRITICAL per Pitfall 2: Each tenant sync gets its own context. // The ldapService.syncUsersForTenant method receives tenantId explicitly // and creates a forTenant scoped client for all DB operations. const result = await this.ldapService.syncUsersForTenant( { id: config.id, tenantId: config.tenantId, serverUrl: config.serverUrl, baseDn: config.baseDn, bindDn: config.bindDn, bindPassword: config.bindPassword, searchFilter: config.searchFilter, groupFilterDns: config.groupFilterDns, userExcludeList: config.userExcludeList, fieldMappings: config.fieldMappings, }, config.tenantId, ); this.logger.log( `LDAP sync completed for tenant ${config.tenantId}: ` + `created=${result.created}, updated=${result.updated}, deactivated=${result.deactivated}` + (result.errors.length > 0 ? `, errors=${result.errors.length}` : ''), ); } catch (error: unknown) { // One tenant's failure must not block others const message = error instanceof Error ? error.message : 'Unknown error'; this.logger.error( `LDAP sync failed for tenant ${config.tenantId}: ${message}`, ); } } } }