feat(ldap): allow testing connection before saving a config
"Verbindung testen" only appeared after a config was already saved, so a fresh/never-configured tenant had no way to validate connection details at all. Now the button is always visible; POST /ldap/test-connection accepts optional ad-hoc serverUrl/bindDn/bindPassword and falls back field-by-field to the saved config for anything omitted (bindPassword in particular, since the form never re-sends the masked saved password). Verified locally: tested with typed-but-unsaved values (base DN left blank, nothing saved) and got a real connection attempt/error back, not a 404 "no config" response. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -53,6 +53,25 @@ export class CreateLdapConfigDto {
|
|||||||
*/
|
*/
|
||||||
export class UpdateLdapConfigDto extends PartialType(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).
|
* DTO for creating a field mapping entry (D-17).
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { Roles } from '../auth/decorators/roles.decorator';
|
|||||||
import {
|
import {
|
||||||
CreateFieldMappingDto,
|
CreateFieldMappingDto,
|
||||||
CreateLdapConfigDto,
|
CreateLdapConfigDto,
|
||||||
|
TestConnectionDto,
|
||||||
UpdateLdapConfigDto,
|
UpdateLdapConfigDto,
|
||||||
} from './dto/ldap-config.dto';
|
} from './dto/ldap-config.dto';
|
||||||
import { LdapConfigService } from './ldap-config.service';
|
import { LdapConfigService } from './ldap-config.service';
|
||||||
@@ -108,27 +109,35 @@ export class LdapController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* POST /ldap/test-connection - Test LDAP connection with current config.
|
* POST /ldap/test-connection - Test an LDAP connection.
|
||||||
* Returns success/failure with error message.
|
*
|
||||||
|
* 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')
|
@Post('test-connection')
|
||||||
@Roles(Role.ADMIN, Role.SUPER_ADMIN)
|
@Roles(Role.ADMIN, Role.SUPER_ADMIN)
|
||||||
async testConnection(@Req() req: any) {
|
async testConnection(@Req() req: any, @Body() dto: TestConnectionDto) {
|
||||||
const tenantId = req.tenantId;
|
const tenantId = req.tenantId;
|
||||||
if (!tenantId) {
|
if (!tenantId) {
|
||||||
throw new BadRequestException('No tenant context');
|
throw new BadRequestException('No tenant context');
|
||||||
}
|
}
|
||||||
|
|
||||||
const config = await this.ldapConfigService.getConfig(tenantId);
|
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({
|
return this.ldapService.testConnection({ serverUrl, bindDn, bindPassword });
|
||||||
serverUrl: config.serverUrl,
|
|
||||||
bindDn: config.bindDn,
|
|
||||||
bindPassword: config.bindPassword,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -155,13 +155,26 @@ export default function AdminLdapPage() {
|
|||||||
const handleTestConnection = async () => {
|
const handleTestConnection = async () => {
|
||||||
setTestResult(null);
|
setTestResult(null);
|
||||||
try {
|
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<string, string> = {};
|
||||||
|
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`, {
|
const res = await fetch(`${API_URL}/ldap/test-connection`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
|
body: JSON.stringify(body),
|
||||||
});
|
});
|
||||||
if (res.ok) {
|
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
if (res.ok) {
|
||||||
setTestResult(data);
|
setTestResult(data);
|
||||||
|
} else {
|
||||||
|
setTestResult({ success: false, error: data.message || 'Test failed' });
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
setTestResult({ success: false, error: 'Network error' });
|
setTestResult({ success: false, error: 'Network error' });
|
||||||
@@ -376,15 +389,17 @@ export default function AdminLdapPage() {
|
|||||||
>
|
>
|
||||||
{saving ? tCommon('loading') : t('save')}
|
{saving ? tCommon('loading') : t('save')}
|
||||||
</button>
|
</button>
|
||||||
{config && (
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleTestConnection}
|
onClick={handleTestConnection}
|
||||||
className="rounded-md border border-border px-4 py-2 text-sm font-medium text-foreground hover:bg-muted transition-colors"
|
disabled={
|
||||||
|
!config &&
|
||||||
|
!(formData.serverUrl && formData.bindDn && formData.bindPassword)
|
||||||
|
}
|
||||||
|
className="rounded-md border border-border px-4 py-2 text-sm font-medium text-foreground hover:bg-muted transition-colors disabled:opacity-50"
|
||||||
>
|
>
|
||||||
{t('testConnection')}
|
{t('testConnection')}
|
||||||
</button>
|
</button>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{testResult && (
|
{testResult && (
|
||||||
|
|||||||
Reference in New Issue
Block a user