feat(ldap): individual user search + selective import with dedup
Add an AD single-user search (by cn/sAMAccountName/displayName/mail) and a selective import to the LDAP admin page, alongside the existing group/OU filter. Imported users are deduped against existing ones by (ldapDn, then username): a manually-imported user carries its ldapDn, so a later department/group sync matches and updates it in place instead of creating a duplicate. Search results flag alreadyImported; import skips existing users and links a missing ldapDn. Extracted shared mapEntry/upsertMappedUser helpers so sync and manual import resolve identity identically. Backend: GET /ldap/users/search, POST /ldap/users/import (RFC-4515 escaped query, ADMIN-guarded). 6 new service specs (search flags, create, skip, ldapDn-link, denylist). Full API suite 215 green, both apps tsc clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,7 @@ import {
|
||||
Param,
|
||||
Patch,
|
||||
Post,
|
||||
Query,
|
||||
Req,
|
||||
} from '@nestjs/common';
|
||||
import { Role } from '@prisma/client';
|
||||
@@ -15,6 +16,7 @@ import { Roles } from '../auth/decorators/roles.decorator';
|
||||
import {
|
||||
CreateFieldMappingDto,
|
||||
CreateLdapConfigDto,
|
||||
ImportUsersDto,
|
||||
TestConnectionDto,
|
||||
UpdateLdapConfigDto,
|
||||
} from './dto/ldap-config.dto';
|
||||
@@ -166,6 +168,71 @@ export class LdapController {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /ldap/users/search?q=... - Search AD for individual users by a
|
||||
* free-text query. Read-only. Each result is flagged `alreadyImported`.
|
||||
*/
|
||||
@Get('users/search')
|
||||
@Roles(Role.ADMIN, Role.SUPER_ADMIN)
|
||||
async searchUsers(@Req() req: any, @Query('q') q: string) {
|
||||
const tenantId = req.tenantId;
|
||||
if (!tenantId) {
|
||||
throw new BadRequestException('No tenant context');
|
||||
}
|
||||
|
||||
const config = await this.ldapConfigService.getConfig(tenantId);
|
||||
if (!config) {
|
||||
throw new NotFoundException('No LDAP config found for this tenant');
|
||||
}
|
||||
|
||||
return this.ldapService.searchUsers(
|
||||
{
|
||||
serverUrl: config.serverUrl,
|
||||
baseDn: config.baseDn,
|
||||
bindDn: config.bindDn,
|
||||
bindPassword: config.bindPassword,
|
||||
},
|
||||
tenantId,
|
||||
q ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /ldap/users/import - Import specific AD users by DN (from the user
|
||||
* search). Idempotent and non-deactivating: existing users are skipped, so
|
||||
* a later department/group sync never creates a duplicate.
|
||||
*/
|
||||
@Post('users/import')
|
||||
@Roles(Role.ADMIN, Role.SUPER_ADMIN)
|
||||
async importUsers(@Req() req: any, @Body() dto: ImportUsersDto) {
|
||||
const tenantId = req.tenantId;
|
||||
if (!tenantId) {
|
||||
throw new BadRequestException('No tenant context');
|
||||
}
|
||||
|
||||
const config = await this.ldapConfigService.getConfig(tenantId);
|
||||
if (!config) {
|
||||
throw new NotFoundException('No LDAP config found for this tenant');
|
||||
}
|
||||
|
||||
return this.ldapService.importUsersByDn(
|
||||
{
|
||||
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,
|
||||
},
|
||||
tenantId,
|
||||
dto.dns,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /ldap/sync - Trigger manual sync (D-14 "LDAP synchronisieren" button).
|
||||
* Returns sync results with created/updated/deactivated counts.
|
||||
|
||||
Reference in New Issue
Block a user