diff --git a/backend/internal/api/handlers/proxy_host_handler.go b/backend/internal/api/handlers/proxy_host_handler.go index 00e6c885..d3321448 100644 --- a/backend/internal/api/handlers/proxy_host_handler.go +++ b/backend/internal/api/handlers/proxy_host_handler.go @@ -216,6 +216,38 @@ func (h *ProxyHostHandler) resolveAccessListReference(value any) (*uint, error) return &id, nil } +func (h *ProxyHostHandler) resolveSecurityHeaderProfileReference(value any) (*uint, error) { + if value == nil { + return nil, nil + } + + parsedID, _, parseErr := parseNullableUintField(value, "security_header_profile_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 profile models.SecurityHeaderProfile + if err := h.db.Select("id").Where("uuid = ?", trimmed).First(&profile).Error; err != nil { + if err == gorm.ErrRecordNotFound { + return nil, fmt.Errorf("security header profile not found") + } + return nil, fmt.Errorf("failed to resolve security header profile") + } + + id := profile.ID + return &id, nil +} + func parseForwardPortField(value any) (int, error) { switch v := value.(type) { case float64: @@ -301,6 +333,15 @@ func (h *ProxyHostHandler) Create(c *gin.Context) { payload["access_list_id"] = resolvedAccessListID } + if rawSecurityHeaderRef, ok := payload["security_header_profile_id"]; ok { + resolvedSecurityHeaderID, resolveErr := h.resolveSecurityHeaderProfileReference(rawSecurityHeaderRef) + if resolveErr != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": resolveErr.Error()}) + return + } + payload["security_header_profile_id"] = resolvedSecurityHeaderID + } + payloadBytes, marshalErr := json.Marshal(payload) if marshalErr != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request payload"}) @@ -508,12 +549,12 @@ func (h *ProxyHostHandler) Update(c *gin.Context) { // Security Header Profile: update only if provided if v, ok := payload["security_header_profile_id"]; ok { - parsedID, _, parseErr := parseNullableUintField(v, "security_header_profile_id") - if parseErr != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": parseErr.Error()}) + resolvedSecurityHeaderID, resolveErr := h.resolveSecurityHeaderProfileReference(v) + if resolveErr != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": resolveErr.Error()}) return } - host.SecurityHeaderProfileID = parsedID + host.SecurityHeaderProfileID = resolvedSecurityHeaderID } // Locations: replace only if provided diff --git a/frontend/src/api/proxyHosts.ts b/frontend/src/api/proxyHosts.ts index c92efb78..d8a3dd23 100644 --- a/frontend/src/api/proxyHosts.ts +++ b/frontend/src/api/proxyHosts.ts @@ -49,10 +49,10 @@ export interface ProxyHost { description: string; type: string; } | null; - security_header_profile_id?: number | null; + security_header_profile_id?: number | string | null; dns_provider_id?: number | null; security_header_profile?: { - id: number; + id?: number; uuid: string; name: string; description: string; diff --git a/frontend/src/components/ProxyHostForm.tsx b/frontend/src/components/ProxyHostForm.tsx index 817a5fd8..cfdbeb28 100644 --- a/frontend/src/components/ProxyHostForm.tsx +++ b/frontend/src/components/ProxyHostForm.tsx @@ -124,7 +124,7 @@ function buildInitialFormData(host?: ProxyHost): Partial & { enabled: host?.enabled ?? true, certificate_id: host?.certificate_id, access_list_id: host?.access_list?.uuid ?? host?.access_list_id, - security_header_profile_id: host?.security_header_profile_id, + security_header_profile_id: host?.security_header_profile?.uuid ?? host?.security_header_profile_id, dns_provider_id: host?.dns_provider_id || null, } } @@ -173,6 +173,20 @@ function normalizeAccessListReference(value: unknown): number | string | null | return trimmed === '' ? null : trimmed } +function normalizeSecurityHeaderReference(value: unknown): number | string | null | undefined { + const numericValue = normalizeNullableID(value) + if (numericValue !== undefined) { + return numericValue + } + + if (typeof value !== 'string') { + return undefined + } + + const trimmed = value.trim() + return trimmed === '' ? null : trimmed +} + function resolveSelectToken(value: number | string | null | undefined): string { if (value === null || value === undefined) { return 'none' @@ -546,7 +560,7 @@ export default function ProxyHostForm({ host, onSubmit, onCancel }: ProxyHostFor const submitPayload: Partial = { ...payloadWithoutUptime, access_list_id: normalizeAccessListReference(payloadWithoutUptime.access_list_id), - security_header_profile_id: normalizeNullableID(payloadWithoutUptime.security_header_profile_id), + security_header_profile_id: normalizeSecurityHeaderReference(payloadWithoutUptime.security_header_profile_id), } const res = await onSubmit(submitPayload)