feat(dns): add custom DNS provider plugin system

- Add plugin interface with lifecycle hooks (Init/Cleanup)
- Implement thread-safe provider registry
- Add plugin loader with SHA-256 signature verification
- Migrate 10 built-in providers to registry pattern
- Add multi-credential support to plugin interface
- Create plugin management UI with enable/disable controls
- Add dynamic credential fields based on provider metadata
- Include PowerDNS example plugin
- Add comprehensive user & developer documentation
- Fix frontend test hang (33min → 1.5min, 22x faster)

Platform: Linux/macOS only (Go plugin limitation)
Security: Signature verification, directory permission checks

Backend coverage: 85.1%
Frontend coverage: 85.31%

Closes: DNS Challenge Future Features - Phase 5
This commit is contained in:
GitHub Actions
2026-01-07 02:54:01 +00:00
parent 048b0c10a7
commit b86aa3921b
48 changed files with 8152 additions and 117 deletions

View File

@@ -22,6 +22,7 @@ import { useDNSProviderTypes, useDNSProviderMutations, type DNSProvider } from '
import type { DNSProviderRequest, DNSProviderTypeInfo } from '../api/dnsProviders'
import { defaultProviderSchemas } from '../data/dnsProviderSchemas'
import { useEnableMultiCredentials, useCredentials } from '../hooks/useCredentials'
import { useProviderFields } from '../hooks/usePlugins'
import CredentialManager from './CredentialManager'
interface DNSProviderFormProps {
@@ -45,6 +46,7 @@ export default function DNSProviderForm({
const [name, setName] = useState('')
const [providerType, setProviderType] = useState<string>('')
const { data: dynamicFields } = useProviderFields(providerType)
const [credentials, setCredentials] = useState<Record<string, string>>({})
const [propagationTimeout, setPropagationTimeout] = useState(120)
const [pollingInterval, setPollingInterval] = useState(5)
@@ -84,6 +86,21 @@ export default function DNSProviderForm({
const getSelectedProviderInfo = (): DNSProviderTypeInfo | undefined => {
if (!providerType) return undefined
// Prefer dynamic fields from API if available
if (dynamicFields) {
return {
type: dynamicFields.type as any,
name: dynamicFields.name,
fields: [
...dynamicFields.required_fields.map(f => ({ ...f, required: true })),
...dynamicFields.optional_fields.map(f => ({ ...f, required: false })),
],
documentation_url: '',
}
}
// Fallback to static types or schemas
return (
providerTypes?.find((pt) => pt.type === providerType) ||
(defaultProviderSchemas[providerType as keyof typeof defaultProviderSchemas] as DNSProviderTypeInfo)