diff --git a/apps/api/src/ldap/dto/ldap-config.dto.ts b/apps/api/src/ldap/dto/ldap-config.dto.ts index 286b44e..a9798a2 100644 --- a/apps/api/src/ldap/dto/ldap-config.dto.ts +++ b/apps/api/src/ldap/dto/ldap-config.dto.ts @@ -53,6 +53,25 @@ export class CreateLdapConfigDto { */ export class UpdateLdapConfigDto extends PartialType(CreateLdapConfigDto) {} +/** + * DTO for testing a connection with ad-hoc (not-yet-saved) values, so an + * admin can validate server/bindDn/bindPassword before persisting a config. + * When omitted, the controller falls back to the tenant's saved config. + */ +export class TestConnectionDto { + @IsUrl({ protocols: ['ldap', 'ldaps'], require_tld: false }) + @IsOptional() + serverUrl?: string; + + @IsString() + @IsOptional() + bindDn?: string; + + @IsString() + @IsOptional() + bindPassword?: string; +} + /** * DTO for creating a field mapping entry (D-17). */ diff --git a/apps/api/src/ldap/ldap.controller.ts b/apps/api/src/ldap/ldap.controller.ts index f61e674..cdfde32 100644 --- a/apps/api/src/ldap/ldap.controller.ts +++ b/apps/api/src/ldap/ldap.controller.ts @@ -15,6 +15,7 @@ import { Roles } from '../auth/decorators/roles.decorator'; import { CreateFieldMappingDto, CreateLdapConfigDto, + TestConnectionDto, UpdateLdapConfigDto, } from './dto/ldap-config.dto'; import { LdapConfigService } from './ldap-config.service'; @@ -108,27 +109,35 @@ export class LdapController { } /** - * POST /ldap/test-connection - Test LDAP connection with current config. - * Returns success/failure with error message. + * POST /ldap/test-connection - Test an LDAP connection. + * + * Accepts optional ad-hoc serverUrl/bindDn/bindPassword so an admin can + * validate connection details before ever saving a config. Any field left + * out (e.g. bindPassword, which the form never re-sends once masked) + * falls back to the tenant's saved config. If no config is saved yet and + * the body doesn't supply all three fields, there is nothing to test. */ @Post('test-connection') @Roles(Role.ADMIN, Role.SUPER_ADMIN) - async testConnection(@Req() req: any) { + async testConnection(@Req() req: any, @Body() dto: TestConnectionDto) { 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'); + + const serverUrl = dto.serverUrl || config?.serverUrl; + const bindDn = dto.bindDn || config?.bindDn; + const bindPassword = dto.bindPassword || config?.bindPassword; + + if (!serverUrl || !bindDn || !bindPassword) { + throw new BadRequestException( + 'serverUrl, bindDn, and bindPassword are required (either provided directly or from a saved config)', + ); } - return this.ldapService.testConnection({ - serverUrl: config.serverUrl, - bindDn: config.bindDn, - bindPassword: config.bindPassword, - }); + return this.ldapService.testConnection({ serverUrl, bindDn, bindPassword }); } /** diff --git a/apps/web/src/app/(portal)/admin/ldap/page.tsx b/apps/web/src/app/(portal)/admin/ldap/page.tsx index fa611bf..67d3a38 100644 --- a/apps/web/src/app/(portal)/admin/ldap/page.tsx +++ b/apps/web/src/app/(portal)/admin/ldap/page.tsx @@ -155,13 +155,26 @@ export default function AdminLdapPage() { const handleTestConnection = async () => { setTestResult(null); try { + // Send the currently entered values so a connection can be validated + // before ever saving a config. bindPassword is omitted when blank so + // the backend falls back to the saved config's password (masked + // fields never get re-sent once a config already exists). + const body: Record = {}; + if (formData.serverUrl) body.serverUrl = formData.serverUrl; + if (formData.bindDn) body.bindDn = formData.bindDn; + if (formData.bindPassword) body.bindPassword = formData.bindPassword; + const res = await fetch(`${API_URL}/ldap/test-connection`, { method: 'POST', + headers: { 'Content-Type': 'application/json' }, credentials: 'include', + body: JSON.stringify(body), }); + const data = await res.json(); if (res.ok) { - const data = await res.json(); setTestResult(data); + } else { + setTestResult({ success: false, error: data.message || 'Test failed' }); } } catch { setTestResult({ success: false, error: 'Network error' }); @@ -376,15 +389,17 @@ export default function AdminLdapPage() { > {saving ? tCommon('loading') : t('save')} - {config && ( - - )} + {testResult && (