diff --git a/backend/internal/api/handlers/proxy_host_handler.go b/backend/internal/api/handlers/proxy_host_handler.go index 6149e47d..686a8c77 100644 --- a/backend/internal/api/handlers/proxy_host_handler.go +++ b/backend/internal/api/handlers/proxy_host_handler.go @@ -248,6 +248,38 @@ func (h *ProxyHostHandler) resolveSecurityHeaderProfileReference(value any) (*ui return &id, nil } +func (h *ProxyHostHandler) resolveCertificateReference(value any) (*uint, error) { + if value == nil { + return nil, nil + } + + parsedID, _, parseErr := parseNullableUintField(value, "certificate_id") + if parseErr == nil { + return parsedID, nil + } + + uuidValue, isString := value.(string) + if !isString { + return nil, parseErr + } + + trimmed := strings.TrimSpace(uuidValue) + if trimmed == "" { + return nil, nil + } + + var cert models.SSLCertificate + if err := h.db.Select("id").Where("uuid = ?", trimmed).First(&cert).Error; err != nil { + if err == gorm.ErrRecordNotFound { + return nil, fmt.Errorf("certificate not found") + } + return nil, fmt.Errorf("failed to resolve certificate") + } + + id := cert.ID + return &id, nil +} + func parseForwardPortField(value any) (int, error) { switch v := value.(type) { case float64: @@ -342,6 +374,15 @@ func (h *ProxyHostHandler) Create(c *gin.Context) { payload["security_header_profile_id"] = resolvedSecurityHeaderID } + if rawCertRef, ok := payload["certificate_id"]; ok { + resolvedCertID, resolveErr := h.resolveCertificateReference(rawCertRef) + if resolveErr != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": resolveErr.Error()}) + return + } + payload["certificate_id"] = resolvedCertID + } + payloadBytes, marshalErr := json.Marshal(payload) if marshalErr != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request payload"}) @@ -523,12 +564,12 @@ func (h *ProxyHostHandler) Update(c *gin.Context) { // Nullable foreign keys if v, ok := payload["certificate_id"]; ok { - parsedID, _, parseErr := parseNullableUintField(v, "certificate_id") - if parseErr != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": parseErr.Error()}) + resolvedCertID, resolveErr := h.resolveCertificateReference(v) + if resolveErr != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": resolveErr.Error()}) return } - host.CertificateID = parsedID + host.CertificateID = resolvedCertID } if v, ok := payload["access_list_id"]; ok { resolvedAccessListID, resolveErr := h.resolveAccessListReference(v) diff --git a/frontend/src/api/proxyHosts.ts b/frontend/src/api/proxyHosts.ts index d8a3dd23..d00461b0 100644 --- a/frontend/src/api/proxyHosts.ts +++ b/frontend/src/api/proxyHosts.ts @@ -9,7 +9,7 @@ export interface Location { } export interface Certificate { - id: number; + id?: number; uuid: string; name: string; provider: string; @@ -40,7 +40,7 @@ export interface ProxyHost { advanced_config?: string; advanced_config_backup?: string; enabled: boolean; - certificate_id?: number | null; + certificate_id?: number | string | null; certificate?: Certificate | null; access_list_id?: number | string | null; access_list?: { diff --git a/frontend/src/components/ProxyHostForm.tsx b/frontend/src/components/ProxyHostForm.tsx index 4cd0181f..549ed826 100644 --- a/frontend/src/components/ProxyHostForm.tsx +++ b/frontend/src/components/ProxyHostForm.tsx @@ -123,7 +123,7 @@ function buildInitialFormData(host?: ProxyHost): Partial & { application: (host?.application || 'none') as ApplicationPreset, advanced_config: host?.advanced_config || '', enabled: host?.enabled ?? true, - certificate_id: host?.certificate_id, + certificate_id: host?.certificate?.uuid ?? host?.certificate_id, access_list_id: host?.access_list?.uuid ?? host?.access_list_id, security_header_profile_id: host?.security_header_profile?.uuid ?? host?.security_header_profile_id, dns_provider_id: host?.dns_provider_id || null, @@ -249,9 +249,10 @@ function getEntityToken(entity: { id?: number; uuid?: string }): string | null { } export default function ProxyHostForm({ host, onSubmit, onCancel }: ProxyHostFormProps) { - type ProxyHostFormState = Omit, 'access_list_id' | 'security_header_profile_id'> & { + type ProxyHostFormState = Omit, 'access_list_id' | 'security_header_profile_id' | 'certificate_id'> & { access_list_id?: number | string | null security_header_profile_id?: number | string | null + certificate_id?: number | string | null addUptime?: boolean uptimeInterval?: number uptimeMaxRetries?: number @@ -562,6 +563,7 @@ export default function ProxyHostForm({ host, onSubmit, onCancel }: ProxyHostFor ...payloadWithoutUptime, access_list_id: normalizeAccessListReference(payloadWithoutUptime.access_list_id), security_header_profile_id: normalizeSecurityHeaderReference(payloadWithoutUptime.security_header_profile_id), + certificate_id: normalizeAccessListReference(payloadWithoutUptime.certificate_id), } const res = await onSubmit(submitPayload) @@ -910,18 +912,25 @@ export default function ProxyHostForm({ host, onSubmit, onCancel }: ProxyHostFor - setFormData(prev => ({ ...prev, certificate_id: resolveTokenToFormValue(token) }))} + > - Auto-manage with Let's Encrypt (recommended) - {certificates.map(cert => ( - - {(cert.name || cert.domains)} - {cert.provider ? ` (${cert.provider})` : ''} - - ))} + Auto-manage with Let's Encrypt (recommended) + {certificates.map(cert => { + const token = getEntityToken(cert) + if (!token) return null + return ( + + {cert.name || cert.domains} + {cert.provider ? ` (${cert.provider})` : ''} + + ) + })}