diff --git a/backend/internal/api/handlers/security_handler.go b/backend/internal/api/handlers/security_handler.go index 2cf4be4c..6b8d9e5d 100644 --- a/backend/internal/api/handlers/security_handler.go +++ b/backend/internal/api/handlers/security_handler.go @@ -198,9 +198,43 @@ func (h *SecurityHandler) GetStatus(c *gin.Context) { "mode": aclMode, "enabled": aclEnabled, }, + "config_apply": latestConfigApplyState(h.db), }) } +func latestConfigApplyState(db *gorm.DB) gin.H { + state := gin.H{ + "available": false, + "status": "unknown", + } + + if db == nil { + return state + } + + var latest models.CaddyConfig + err := db.Order("applied_at desc").First(&latest).Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return state + } + return state + } + + status := "failed" + if latest.Success { + status = "applied" + } + + state["available"] = true + state["status"] = status + state["success"] = latest.Success + state["applied_at"] = latest.AppliedAt + state["error_msg"] = latest.ErrorMsg + + return state +} + // GetConfig returns the site security configuration from DB or default func (h *SecurityHandler) GetConfig(c *gin.Context) { cfg, err := h.svc.Get() diff --git a/backend/internal/api/handlers/security_handler_fixed_test.go b/backend/internal/api/handlers/security_handler_fixed_test.go index 2dfdf40b..6148e992 100644 --- a/backend/internal/api/handlers/security_handler_fixed_test.go +++ b/backend/internal/api/handlers/security_handler_fixed_test.go @@ -49,6 +49,10 @@ func TestSecurityHandler_GetStatus_Fixed(t *testing.T) { "mode": "disabled", "enabled": false, }, + "config_apply": map[string]any{ + "available": false, + "status": "unknown", + }, }, }, { @@ -80,6 +84,10 @@ func TestSecurityHandler_GetStatus_Fixed(t *testing.T) { "mode": "enabled", "enabled": true, }, + "config_apply": map[string]any{ + "available": false, + "status": "unknown", + }, }, }, } diff --git a/backend/internal/api/handlers/security_handler_settings_test.go b/backend/internal/api/handlers/security_handler_settings_test.go index 0c1082c2..c351daf8 100644 --- a/backend/internal/api/handlers/security_handler_settings_test.go +++ b/backend/internal/api/handlers/security_handler_settings_test.go @@ -227,6 +227,37 @@ func TestSecurityHandler_GetStatus_RateLimitModeFromSettings(t *testing.T) { rateLimit := response["rate_limit"].(map[string]any) assert.True(t, rateLimit["enabled"].(bool)) + + configApply := response["config_apply"].(map[string]any) + assert.Equal(t, false, configApply["available"]) + assert.Equal(t, "unknown", configApply["status"]) +} + +func TestSecurityHandler_GetStatus_IncludesLatestConfigApplyState(t *testing.T) { + gin.SetMode(gin.TestMode) + db := setupTestDB(t) + require.NoError(t, db.AutoMigrate(&models.Setting{}, &models.CaddyConfig{})) + + require.NoError(t, db.Create(&models.CaddyConfig{Success: true, ErrorMsg: ""}).Error) + + handler := NewSecurityHandler(config.SecurityConfig{CerberusEnabled: true}, db, nil) + router := gin.New() + router.GET("/security/status", handler.GetStatus) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/security/status", http.NoBody) + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + + var response map[string]any + err := json.Unmarshal(w.Body.Bytes(), &response) + require.NoError(t, err) + + configApply := response["config_apply"].(map[string]any) + assert.Equal(t, true, configApply["available"]) + assert.Equal(t, "applied", configApply["status"]) + assert.Equal(t, true, configApply["success"]) } func TestSecurityHandler_PatchACL_RequiresAdminWhitelist(t *testing.T) {