mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-04-28 11:30:15 +00:00
The single-tenant lockdown (499ab812e) set mtPersistence to nil but
only patched AISettingsHandler with a legacy fallback. AIHandler (chat
service) and ConfigProfileHandler were missed, so AI features (Patrol,
Chat) failed with "chat service not available" and config profiles
would panic on nil dereference. Wire legacy persistence into both
handlers and add the same fallback to ProfileSuggestionHandler.
Fixes #1322
1004 lines
29 KiB
Go
1004 lines
29 KiB
Go
package api
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/config"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/models"
|
|
"github.com/rs/zerolog/log"
|
|
)
|
|
|
|
// ConfigProfileHandler handles configuration profile operations
|
|
type ConfigProfileHandler struct {
|
|
mtPersistence *config.MultiTenantPersistence
|
|
legacyPersistence *config.ConfigPersistence
|
|
validator *models.ProfileValidator
|
|
mu sync.RWMutex
|
|
suggestionHandler *ProfileSuggestionHandler
|
|
}
|
|
|
|
// NewConfigProfileHandler creates a new handler
|
|
func NewConfigProfileHandler(mtp *config.MultiTenantPersistence) *ConfigProfileHandler {
|
|
return &ConfigProfileHandler{
|
|
mtPersistence: mtp,
|
|
validator: models.NewProfileValidator(),
|
|
}
|
|
}
|
|
|
|
// SetLegacyPersistence sets the single-tenant persistence fallback.
|
|
func (h *ConfigProfileHandler) SetLegacyPersistence(p *config.ConfigPersistence) {
|
|
h.legacyPersistence = p
|
|
}
|
|
|
|
// getPersistence resolves the persistence instance for the current tenant
|
|
func (h *ConfigProfileHandler) getPersistence(ctx context.Context) (*config.ConfigPersistence, error) {
|
|
if h.mtPersistence != nil {
|
|
orgID := GetOrgID(ctx)
|
|
return h.mtPersistence.GetPersistence(orgID)
|
|
}
|
|
if h.legacyPersistence != nil {
|
|
return h.legacyPersistence, nil
|
|
}
|
|
return nil, fmt.Errorf("no persistence available")
|
|
}
|
|
|
|
// SetAIHandler sets the AI handler for profile suggestions
|
|
func (h *ConfigProfileHandler) SetAIHandler(aiHandler *AIHandler) {
|
|
h.suggestionHandler = NewProfileSuggestionHandler(h.mtPersistence, h.legacyPersistence, aiHandler)
|
|
}
|
|
|
|
// ServeHTTP implements the http.Handler interface
|
|
func (h *ConfigProfileHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
// Simple routing
|
|
path := strings.TrimSuffix(r.URL.Path, "/")
|
|
|
|
if path == "" || path == "/" {
|
|
if r.Method == http.MethodGet {
|
|
h.ListProfiles(w, r)
|
|
return
|
|
} else if r.Method == http.MethodPost {
|
|
h.CreateProfile(w, r)
|
|
return
|
|
}
|
|
} else if path == "/assignments" {
|
|
if r.Method == http.MethodGet {
|
|
h.ListAssignments(w, r)
|
|
return
|
|
} else if r.Method == http.MethodPost {
|
|
h.AssignProfile(w, r)
|
|
return
|
|
}
|
|
} else if strings.HasPrefix(path, "/assignments/") {
|
|
if r.Method == http.MethodDelete {
|
|
agentID := strings.TrimPrefix(path, "/assignments/")
|
|
h.UnassignProfile(w, r, agentID)
|
|
return
|
|
}
|
|
} else if path == "/schema" {
|
|
// GET /schema - Return config key definitions
|
|
if r.Method == http.MethodGet {
|
|
h.GetConfigSchema(w, r)
|
|
return
|
|
}
|
|
} else if path == "/validate" {
|
|
// POST /validate - Validate a config without saving
|
|
if r.Method == http.MethodPost {
|
|
h.ValidateConfig(w, r)
|
|
return
|
|
}
|
|
} else if path == "/suggestions" {
|
|
// POST /suggestions - AI-assisted profile suggestion
|
|
if r.Method == http.MethodPost {
|
|
if h.suggestionHandler != nil {
|
|
h.suggestionHandler.HandleSuggestProfile(w, r)
|
|
} else {
|
|
http.Error(w, "Pulse Assistant service not configured", http.StatusServiceUnavailable)
|
|
}
|
|
return
|
|
}
|
|
} else if path == "/changelog" {
|
|
// GET /changelog - Return profile change history
|
|
if r.Method == http.MethodGet {
|
|
h.GetChangeLog(w, r)
|
|
return
|
|
}
|
|
} else if path == "/deployments" {
|
|
// GET /deployments - Return deployment status
|
|
// POST /deployments - Update deployment status from agent
|
|
if r.Method == http.MethodGet {
|
|
h.GetDeploymentStatus(w, r)
|
|
return
|
|
} else if r.Method == http.MethodPost {
|
|
h.UpdateDeploymentStatus(w, r)
|
|
return
|
|
}
|
|
} else if strings.HasSuffix(path, "/versions") {
|
|
// GET /{id}/versions - Get version history for a profile
|
|
id := strings.TrimSuffix(path, "/versions")
|
|
id = strings.TrimPrefix(id, "/")
|
|
if r.Method == http.MethodGet {
|
|
h.GetProfileVersions(w, r, id)
|
|
return
|
|
}
|
|
} else if strings.Contains(path, "/rollback/") {
|
|
// POST /{id}/rollback/{version} - Rollback to a specific version
|
|
parts := strings.Split(path, "/")
|
|
if len(parts) >= 3 && r.Method == http.MethodPost {
|
|
// parts: ["", "id", "rollback", "version"]
|
|
id := parts[1]
|
|
version := parts[len(parts)-1]
|
|
h.RollbackProfile(w, r, id, version)
|
|
return
|
|
}
|
|
} else {
|
|
// ID parameters
|
|
// Expecting /{id}
|
|
id := strings.TrimPrefix(path, "/")
|
|
if r.Method == http.MethodGet {
|
|
h.GetProfile(w, r, id)
|
|
return
|
|
} else if r.Method == http.MethodPut {
|
|
h.UpdateProfile(w, r, id)
|
|
return
|
|
} else if r.Method == http.MethodDelete {
|
|
h.DeleteProfile(w, r, id)
|
|
return
|
|
}
|
|
}
|
|
|
|
http.Error(w, "Not found", http.StatusNotFound)
|
|
}
|
|
|
|
// ListProfiles returns all profiles
|
|
func (h *ConfigProfileHandler) ListProfiles(w http.ResponseWriter, r *http.Request) {
|
|
persistence, err := h.getPersistence(r.Context())
|
|
if err != nil {
|
|
log.Error().Err(err).Msg("Failed to get persistence for tenant")
|
|
http.Error(w, "Tenant configuration error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
profiles, err := persistence.LoadAgentProfiles()
|
|
if err != nil {
|
|
log.Error().Err(err).Msg("Failed to load profiles")
|
|
http.Error(w, "Failed to load profiles", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
// Return empty array instead of null
|
|
if profiles == nil {
|
|
profiles = []models.AgentProfile{}
|
|
}
|
|
json.NewEncoder(w).Encode(profiles)
|
|
}
|
|
|
|
// CreateProfile creates a new profile
|
|
func (h *ConfigProfileHandler) CreateProfile(w http.ResponseWriter, r *http.Request) {
|
|
var input struct {
|
|
models.AgentProfile
|
|
ChangeNote string `json:"change_note,omitempty"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
|
|
http.Error(w, "Invalid request body", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
if input.Name == "" {
|
|
http.Error(w, "Name is required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Validate configuration
|
|
if input.Config != nil {
|
|
result := h.validator.Validate(input.Config)
|
|
if !result.Valid {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
|
"error": "validation_failed",
|
|
"message": "Configuration validation failed",
|
|
"errors": result.Errors,
|
|
"warnings": result.Warnings,
|
|
})
|
|
return
|
|
}
|
|
}
|
|
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
|
|
persistence, err := h.getPersistence(r.Context())
|
|
if err != nil {
|
|
log.Error().Err(err).Msg("Failed to get persistence for tenant")
|
|
http.Error(w, "Tenant configuration error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
profiles, err := persistence.LoadAgentProfiles()
|
|
if err != nil {
|
|
http.Error(w, "Failed to load profiles", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// Get username from context
|
|
username := getUsernameFromRequest(r)
|
|
|
|
input.ID = uuid.New().String()
|
|
input.Version = 1
|
|
input.CreatedAt = time.Now()
|
|
input.UpdatedAt = time.Now()
|
|
input.CreatedBy = username
|
|
input.UpdatedBy = username
|
|
|
|
profiles = append(profiles, input.AgentProfile)
|
|
|
|
if err := persistence.SaveAgentProfiles(profiles); err != nil {
|
|
log.Error().Err(err).Msg("Failed to save profiles")
|
|
http.Error(w, "Failed to save profile", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// Save initial version to history
|
|
version := models.AgentProfileVersion{
|
|
ProfileID: input.ID,
|
|
Version: 1,
|
|
Name: input.Name,
|
|
Description: input.Description,
|
|
Config: input.Config,
|
|
ParentID: input.ParentID,
|
|
CreatedAt: input.CreatedAt,
|
|
CreatedBy: username,
|
|
ChangeNote: input.ChangeNote,
|
|
}
|
|
h.saveVersionHistory(persistence, version)
|
|
|
|
// Log change
|
|
h.logChange(persistence, models.ProfileChangeLog{
|
|
ID: uuid.New().String(),
|
|
ProfileID: input.ID,
|
|
ProfileName: input.Name,
|
|
Action: "create",
|
|
NewVersion: 1,
|
|
User: username,
|
|
Timestamp: time.Now(),
|
|
Details: input.ChangeNote,
|
|
})
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(input.AgentProfile)
|
|
}
|
|
|
|
// getUsernameFromRequest extracts the username from the request context
|
|
func getUsernameFromRequest(r *http.Request) string {
|
|
if username, ok := r.Context().Value("username").(string); ok {
|
|
return username
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// UpdateProfile updates an existing profile
|
|
func (h *ConfigProfileHandler) UpdateProfile(w http.ResponseWriter, r *http.Request, id string) {
|
|
if id == "" {
|
|
http.Error(w, "ID is required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
var input struct {
|
|
models.AgentProfile
|
|
ChangeNote string `json:"change_note,omitempty"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
|
|
http.Error(w, "Invalid request body", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Validate configuration
|
|
if input.Config != nil {
|
|
result := h.validator.Validate(input.Config)
|
|
if !result.Valid {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
|
"error": "validation_failed",
|
|
"message": "Configuration validation failed",
|
|
"errors": result.Errors,
|
|
"warnings": result.Warnings,
|
|
})
|
|
return
|
|
}
|
|
}
|
|
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
|
|
persistence, err := h.getPersistence(r.Context())
|
|
if err != nil {
|
|
log.Error().Err(err).Msg("Failed to get persistence for tenant")
|
|
http.Error(w, "Tenant configuration error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
profiles, err := persistence.LoadAgentProfiles()
|
|
if err != nil {
|
|
http.Error(w, "Failed to load profiles", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
username := getUsernameFromRequest(r)
|
|
found := false
|
|
var oldVersion int
|
|
var updatedProfile models.AgentProfile
|
|
|
|
for i, p := range profiles {
|
|
if p.ID == id {
|
|
oldVersion = p.Version
|
|
profiles[i].Name = input.Name
|
|
profiles[i].Description = input.Description
|
|
profiles[i].Config = input.Config
|
|
profiles[i].ParentID = input.ParentID
|
|
profiles[i].Version = p.Version + 1
|
|
profiles[i].UpdatedAt = time.Now()
|
|
profiles[i].UpdatedBy = username
|
|
updatedProfile = profiles[i]
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !found {
|
|
http.Error(w, "Profile not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
if err := persistence.SaveAgentProfiles(profiles); err != nil {
|
|
log.Error().Err(err).Msg("Failed to save profiles")
|
|
http.Error(w, "Failed to save profile", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// Save new version to history
|
|
version := models.AgentProfileVersion{
|
|
ProfileID: id,
|
|
Version: updatedProfile.Version,
|
|
Name: updatedProfile.Name,
|
|
Description: updatedProfile.Description,
|
|
Config: updatedProfile.Config,
|
|
ParentID: updatedProfile.ParentID,
|
|
CreatedAt: updatedProfile.UpdatedAt,
|
|
CreatedBy: username,
|
|
ChangeNote: input.ChangeNote,
|
|
}
|
|
h.saveVersionHistory(persistence, version)
|
|
|
|
// Log change
|
|
h.logChange(persistence, models.ProfileChangeLog{
|
|
ID: uuid.New().String(),
|
|
ProfileID: id,
|
|
ProfileName: updatedProfile.Name,
|
|
Action: "update",
|
|
OldVersion: oldVersion,
|
|
NewVersion: updatedProfile.Version,
|
|
User: username,
|
|
Timestamp: time.Now(),
|
|
Details: input.ChangeNote,
|
|
})
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(updatedProfile)
|
|
}
|
|
|
|
// DeleteProfile deletes a profile
|
|
func (h *ConfigProfileHandler) DeleteProfile(w http.ResponseWriter, r *http.Request, id string) {
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
|
|
persistence, err := h.getPersistence(r.Context())
|
|
if err != nil {
|
|
log.Error().Err(err).Msg("Failed to get persistence for tenant")
|
|
http.Error(w, "Tenant configuration error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
profiles, err := persistence.LoadAgentProfiles()
|
|
if err != nil {
|
|
http.Error(w, "Failed to load profiles", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
var deletedProfile *models.AgentProfile
|
|
newProfiles := []models.AgentProfile{}
|
|
for _, p := range profiles {
|
|
if p.ID != id {
|
|
newProfiles = append(newProfiles, p)
|
|
} else {
|
|
deletedProfile = &p
|
|
}
|
|
}
|
|
|
|
if len(newProfiles) == len(profiles) {
|
|
http.Error(w, "Profile not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
if err := persistence.SaveAgentProfiles(newProfiles); err != nil {
|
|
log.Error().Err(err).Msg("Failed to save profiles")
|
|
http.Error(w, "Failed to delete profile", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
assignments, err := persistence.LoadAgentProfileAssignments()
|
|
if err != nil {
|
|
log.Error().Err(err).Msg("Failed to load assignments for profile cleanup")
|
|
http.Error(w, "Failed to delete profile assignments", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
cleaned := assignments[:0]
|
|
for _, a := range assignments {
|
|
if a.ProfileID != id {
|
|
cleaned = append(cleaned, a)
|
|
}
|
|
}
|
|
|
|
if len(cleaned) != len(assignments) {
|
|
if err := persistence.SaveAgentProfileAssignments(cleaned); err != nil {
|
|
log.Error().Err(err).Msg("Failed to clean up assignments for deleted profile")
|
|
http.Error(w, "Failed to delete profile assignments", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
}
|
|
|
|
// Log deletion
|
|
username := getUsernameFromRequest(r)
|
|
if deletedProfile != nil {
|
|
h.logChange(persistence, models.ProfileChangeLog{
|
|
ID: uuid.New().String(),
|
|
ProfileID: id,
|
|
ProfileName: deletedProfile.Name,
|
|
Action: "delete",
|
|
OldVersion: deletedProfile.Version,
|
|
User: username,
|
|
Timestamp: time.Now(),
|
|
})
|
|
}
|
|
|
|
w.WriteHeader(http.StatusOK)
|
|
}
|
|
|
|
// ListAssignments returns all assignments
|
|
func (h *ConfigProfileHandler) ListAssignments(w http.ResponseWriter, r *http.Request) {
|
|
persistence, err := h.getPersistence(r.Context())
|
|
if err != nil {
|
|
log.Error().Err(err).Msg("Failed to get persistence for tenant")
|
|
http.Error(w, "Tenant configuration error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
assignments, err := persistence.LoadAgentProfileAssignments()
|
|
if err != nil {
|
|
log.Error().Err(err).Msg("Failed to load assignments")
|
|
http.Error(w, "Failed to load assignments", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
// Return empty array instead of null
|
|
if assignments == nil {
|
|
assignments = []models.AgentProfileAssignment{}
|
|
}
|
|
json.NewEncoder(w).Encode(assignments)
|
|
}
|
|
|
|
// AssignProfile assigns a profile to an agent
|
|
func (h *ConfigProfileHandler) AssignProfile(w http.ResponseWriter, r *http.Request) {
|
|
var input models.AgentProfileAssignment
|
|
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
|
|
http.Error(w, "Invalid request body", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
if input.AgentID == "" || input.ProfileID == "" {
|
|
http.Error(w, "AgentID and ProfileID are required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
|
|
persistence, err := h.getPersistence(r.Context())
|
|
if err != nil {
|
|
log.Error().Err(err).Msg("Failed to get persistence for tenant")
|
|
http.Error(w, "Tenant configuration error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
assignments, err := persistence.LoadAgentProfileAssignments()
|
|
if err != nil {
|
|
http.Error(w, "Failed to load assignments", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// Remove existing assignment for this agent if exists
|
|
newAssignments := []models.AgentProfileAssignment{}
|
|
for _, a := range assignments {
|
|
if a.AgentID != input.AgentID {
|
|
newAssignments = append(newAssignments, a)
|
|
}
|
|
}
|
|
|
|
username := getUsernameFromRequest(r)
|
|
input.UpdatedAt = time.Now()
|
|
input.AssignedBy = username
|
|
newAssignments = append(newAssignments, input)
|
|
|
|
if err := persistence.SaveAgentProfileAssignments(newAssignments); err != nil {
|
|
log.Error().Err(err).Msg("Failed to save assignments")
|
|
http.Error(w, "Failed to save assignment", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// Get profile name for logging
|
|
profiles, _ := persistence.LoadAgentProfiles()
|
|
var profileName string
|
|
for _, p := range profiles {
|
|
if p.ID == input.ProfileID {
|
|
profileName = p.Name
|
|
break
|
|
}
|
|
}
|
|
|
|
// Log assignment
|
|
h.logChange(persistence, models.ProfileChangeLog{
|
|
ID: uuid.New().String(),
|
|
ProfileID: input.ProfileID,
|
|
ProfileName: profileName,
|
|
Action: "assign",
|
|
AgentID: input.AgentID,
|
|
User: username,
|
|
Timestamp: time.Now(),
|
|
})
|
|
|
|
tokenID := ""
|
|
if record := getAPITokenRecordFromRequest(r); record != nil {
|
|
tokenID = record.ID
|
|
}
|
|
LogAuditEventForTenant(GetOrgID(r.Context()), "agent_profile_assigned", username, GetClientIP(r), r.URL.Path, true,
|
|
fmt.Sprintf("agent_id=%s profile_id=%s token_id=%s", input.AgentID, input.ProfileID, tokenID))
|
|
|
|
json.NewEncoder(w).Encode(input)
|
|
}
|
|
|
|
// UnassignProfile removes a profile assignment for an agent.
|
|
func (h *ConfigProfileHandler) UnassignProfile(w http.ResponseWriter, r *http.Request, agentID string) {
|
|
agentID = strings.TrimSpace(agentID)
|
|
if agentID == "" {
|
|
http.Error(w, "AgentID is required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
|
|
persistence, err := h.getPersistence(r.Context())
|
|
if err != nil {
|
|
log.Error().Err(err).Msg("Failed to get persistence for tenant")
|
|
http.Error(w, "Tenant configuration error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
assignments, err := persistence.LoadAgentProfileAssignments()
|
|
if err != nil {
|
|
http.Error(w, "Failed to load assignments", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
var removedAssignment *models.AgentProfileAssignment
|
|
newAssignments := []models.AgentProfileAssignment{}
|
|
for _, a := range assignments {
|
|
if a.AgentID != agentID {
|
|
newAssignments = append(newAssignments, a)
|
|
} else {
|
|
removedAssignment = &a
|
|
}
|
|
}
|
|
|
|
if len(newAssignments) != len(assignments) {
|
|
if err := persistence.SaveAgentProfileAssignments(newAssignments); err != nil {
|
|
log.Error().Err(err).Msg("Failed to save assignments")
|
|
http.Error(w, "Failed to save assignment", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// Log unassignment
|
|
if removedAssignment != nil {
|
|
username := getUsernameFromRequest(r)
|
|
|
|
// Get profile name for logging
|
|
profiles, _ := persistence.LoadAgentProfiles()
|
|
var profileName string
|
|
for _, p := range profiles {
|
|
if p.ID == removedAssignment.ProfileID {
|
|
profileName = p.Name
|
|
break
|
|
}
|
|
}
|
|
|
|
h.logChange(persistence, models.ProfileChangeLog{
|
|
ID: uuid.New().String(),
|
|
ProfileID: removedAssignment.ProfileID,
|
|
ProfileName: profileName,
|
|
Action: "unassign",
|
|
AgentID: agentID,
|
|
User: username,
|
|
Timestamp: time.Now(),
|
|
})
|
|
|
|
tokenID := ""
|
|
if record := getAPITokenRecordFromRequest(r); record != nil {
|
|
tokenID = record.ID
|
|
}
|
|
LogAuditEventForTenant(GetOrgID(r.Context()), "agent_profile_unassigned", username, GetClientIP(r), r.URL.Path, true,
|
|
fmt.Sprintf("agent_id=%s profile_id=%s token_id=%s", agentID, removedAssignment.ProfileID, tokenID))
|
|
}
|
|
}
|
|
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
// GetProfile returns a single profile by ID
|
|
func (h *ConfigProfileHandler) GetProfile(w http.ResponseWriter, r *http.Request, id string) {
|
|
persistence, err := h.getPersistence(r.Context())
|
|
if err != nil {
|
|
log.Error().Err(err).Msg("Failed to get persistence for tenant")
|
|
http.Error(w, "Tenant configuration error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
profiles, err := persistence.LoadAgentProfiles()
|
|
if err != nil {
|
|
log.Error().Err(err).Msg("Failed to load profiles")
|
|
http.Error(w, "Failed to load profiles", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
for _, p := range profiles {
|
|
if p.ID == id {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(p)
|
|
return
|
|
}
|
|
}
|
|
|
|
http.Error(w, "Profile not found", http.StatusNotFound)
|
|
}
|
|
|
|
// GetConfigSchema returns the configuration key definitions
|
|
func (h *ConfigProfileHandler) GetConfigSchema(w http.ResponseWriter, r *http.Request) {
|
|
definitions := models.GetConfigKeyDefinitions()
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(definitions)
|
|
}
|
|
|
|
// ValidateConfig validates a configuration without saving
|
|
func (h *ConfigProfileHandler) ValidateConfig(w http.ResponseWriter, r *http.Request) {
|
|
var config models.AgentConfigMap
|
|
if err := json.NewDecoder(r.Body).Decode(&config); err != nil {
|
|
http.Error(w, "Invalid request body", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
result := h.validator.Validate(config)
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(result)
|
|
}
|
|
|
|
// GetChangeLog returns profile change history
|
|
func (h *ConfigProfileHandler) GetChangeLog(w http.ResponseWriter, r *http.Request) {
|
|
persistence, err := h.getPersistence(r.Context())
|
|
if err != nil {
|
|
log.Error().Err(err).Msg("Failed to get persistence for tenant")
|
|
http.Error(w, "Tenant configuration error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
logs, err := persistence.LoadProfileChangeLogs()
|
|
if err != nil {
|
|
log.Error().Err(err).Msg("Failed to load change logs")
|
|
http.Error(w, "Failed to load change logs", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// Filter by profile_id if specified
|
|
profileID := r.URL.Query().Get("profile_id")
|
|
if profileID != "" {
|
|
filtered := []models.ProfileChangeLog{}
|
|
for _, entry := range logs {
|
|
if entry.ProfileID == profileID {
|
|
filtered = append(filtered, entry)
|
|
}
|
|
}
|
|
logs = filtered
|
|
}
|
|
|
|
// Return empty array instead of null
|
|
if logs == nil {
|
|
logs = []models.ProfileChangeLog{}
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(logs)
|
|
}
|
|
|
|
// GetDeploymentStatus returns deployment status for all agents
|
|
func (h *ConfigProfileHandler) GetDeploymentStatus(w http.ResponseWriter, r *http.Request) {
|
|
persistence, err := h.getPersistence(r.Context())
|
|
if err != nil {
|
|
log.Error().Err(err).Msg("Failed to get persistence for tenant")
|
|
http.Error(w, "Tenant configuration error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
status, err := persistence.LoadProfileDeploymentStatus()
|
|
if err != nil {
|
|
log.Error().Err(err).Msg("Failed to load deployment status")
|
|
http.Error(w, "Failed to load deployment status", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// Filter by agent_id if specified
|
|
agentID := r.URL.Query().Get("agent_id")
|
|
if agentID != "" {
|
|
filtered := []models.ProfileDeploymentStatus{}
|
|
for _, s := range status {
|
|
if s.AgentID == agentID {
|
|
filtered = append(filtered, s)
|
|
}
|
|
}
|
|
status = filtered
|
|
}
|
|
|
|
// Return empty array instead of null
|
|
if status == nil {
|
|
status = []models.ProfileDeploymentStatus{}
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(status)
|
|
}
|
|
|
|
// UpdateDeploymentStatus updates deployment status from an agent
|
|
func (h *ConfigProfileHandler) UpdateDeploymentStatus(w http.ResponseWriter, r *http.Request) {
|
|
var input models.ProfileDeploymentStatus
|
|
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
|
|
http.Error(w, "Invalid request body", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
if input.AgentID == "" || input.ProfileID == "" {
|
|
http.Error(w, "AgentID and ProfileID are required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Validate deployment status
|
|
validStatuses := []string{"pending", "deployed", "failed"}
|
|
validStatus := false
|
|
for _, s := range validStatuses {
|
|
if input.DeploymentStatus == s {
|
|
validStatus = true
|
|
break
|
|
}
|
|
}
|
|
if !validStatus {
|
|
http.Error(w, "Invalid deployment status. Must be 'pending', 'deployed', or 'failed'", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
|
|
persistence, err := h.getPersistence(r.Context())
|
|
if err != nil {
|
|
http.Error(w, "Tenant configuration error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
statuses, err := persistence.LoadProfileDeploymentStatus()
|
|
if err != nil {
|
|
http.Error(w, "Failed to load deployment status", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// Update or add the status
|
|
found := false
|
|
for i, s := range statuses {
|
|
if s.AgentID == input.AgentID && s.ProfileID == input.ProfileID {
|
|
statuses[i].DeployedVersion = input.DeployedVersion
|
|
statuses[i].DeploymentStatus = input.DeploymentStatus
|
|
statuses[i].ErrorMessage = input.ErrorMessage
|
|
statuses[i].LastDeployedAt = time.Now()
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !found {
|
|
input.LastDeployedAt = time.Now()
|
|
statuses = append(statuses, input)
|
|
}
|
|
|
|
if err := persistence.SaveProfileDeploymentStatus(statuses); err != nil {
|
|
log.Error().Err(err).Msg("Failed to save deployment status")
|
|
http.Error(w, "Failed to save deployment status", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(input)
|
|
}
|
|
|
|
// GetProfileVersions returns version history for a profile
|
|
func (h *ConfigProfileHandler) GetProfileVersions(w http.ResponseWriter, r *http.Request, profileID string) {
|
|
persistence, err := h.getPersistence(r.Context())
|
|
if err != nil {
|
|
log.Error().Err(err).Msg("Failed to get persistence for tenant")
|
|
http.Error(w, "Tenant configuration error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
versions, err := persistence.LoadAgentProfileVersions()
|
|
if err != nil {
|
|
log.Error().Err(err).Msg("Failed to load profile versions")
|
|
http.Error(w, "Failed to load profile versions", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// Filter by profile ID
|
|
filtered := []models.AgentProfileVersion{}
|
|
for _, v := range versions {
|
|
if v.ProfileID == profileID {
|
|
filtered = append(filtered, v)
|
|
}
|
|
}
|
|
|
|
// Return empty array instead of null
|
|
if filtered == nil {
|
|
filtered = []models.AgentProfileVersion{}
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(filtered)
|
|
}
|
|
|
|
// RollbackProfile rolls back a profile to a specific version
|
|
func (h *ConfigProfileHandler) RollbackProfile(w http.ResponseWriter, r *http.Request, profileID string, versionStr string) {
|
|
var targetVersion int
|
|
if _, err := fmt.Sscanf(versionStr, "%d", &targetVersion); err != nil {
|
|
http.Error(w, "Invalid version number", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
|
|
persistence, err := h.getPersistence(r.Context())
|
|
if err != nil {
|
|
log.Error().Err(err).Msg("Failed to get persistence for tenant")
|
|
http.Error(w, "Tenant configuration error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// Load version history to find the target version
|
|
versions, err := persistence.LoadAgentProfileVersions()
|
|
if err != nil {
|
|
http.Error(w, "Failed to load profile versions", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
var targetVersionData *models.AgentProfileVersion
|
|
for i, v := range versions {
|
|
if v.ProfileID == profileID && v.Version == targetVersion {
|
|
targetVersionData = &versions[i]
|
|
break
|
|
}
|
|
}
|
|
|
|
if targetVersionData == nil {
|
|
http.Error(w, "Version not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
// Load current profiles
|
|
profiles, err := persistence.LoadAgentProfiles()
|
|
if err != nil {
|
|
http.Error(w, "Failed to load profiles", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
username := getUsernameFromRequest(r)
|
|
found := false
|
|
var oldVersion int
|
|
var updatedProfile models.AgentProfile
|
|
|
|
for i, p := range profiles {
|
|
if p.ID == profileID {
|
|
oldVersion = p.Version
|
|
profiles[i].Name = targetVersionData.Name
|
|
profiles[i].Description = targetVersionData.Description
|
|
profiles[i].Config = targetVersionData.Config
|
|
profiles[i].ParentID = targetVersionData.ParentID
|
|
profiles[i].Version = p.Version + 1
|
|
profiles[i].UpdatedAt = time.Now()
|
|
profiles[i].UpdatedBy = username
|
|
updatedProfile = profiles[i]
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !found {
|
|
http.Error(w, "Profile not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
if err := persistence.SaveAgentProfiles(profiles); err != nil {
|
|
log.Error().Err(err).Msg("Failed to save profiles after rollback")
|
|
http.Error(w, "Failed to rollback profile", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// Save new version to history
|
|
version := models.AgentProfileVersion{
|
|
ProfileID: profileID,
|
|
Version: updatedProfile.Version,
|
|
Name: updatedProfile.Name,
|
|
Description: updatedProfile.Description,
|
|
Config: updatedProfile.Config,
|
|
ParentID: updatedProfile.ParentID,
|
|
CreatedAt: updatedProfile.UpdatedAt,
|
|
CreatedBy: username,
|
|
ChangeNote: fmt.Sprintf("Rolled back to version %d", targetVersion),
|
|
}
|
|
h.saveVersionHistory(persistence, version)
|
|
|
|
// Log rollback
|
|
h.logChange(persistence, models.ProfileChangeLog{
|
|
ID: uuid.New().String(),
|
|
ProfileID: profileID,
|
|
ProfileName: updatedProfile.Name,
|
|
Action: "rollback",
|
|
OldVersion: oldVersion,
|
|
NewVersion: updatedProfile.Version,
|
|
User: username,
|
|
Timestamp: time.Now(),
|
|
Details: fmt.Sprintf("Rolled back from version %d to version %d", oldVersion, targetVersion),
|
|
})
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(updatedProfile)
|
|
}
|
|
|
|
// saveVersionHistory saves a version to the history
|
|
func (h *ConfigProfileHandler) saveVersionHistory(persistence *config.ConfigPersistence, version models.AgentProfileVersion) {
|
|
versions, err := persistence.LoadAgentProfileVersions()
|
|
if err != nil {
|
|
log.Error().Err(err).Msg("Failed to load version history")
|
|
return
|
|
}
|
|
|
|
versions = append(versions, version)
|
|
|
|
if err := persistence.SaveAgentProfileVersions(versions); err != nil {
|
|
log.Error().Err(err).Msg("Failed to save version history")
|
|
}
|
|
}
|
|
|
|
// logChange logs a profile change to the change log
|
|
func (h *ConfigProfileHandler) logChange(persistence *config.ConfigPersistence, entry models.ProfileChangeLog) {
|
|
if err := persistence.AppendProfileChangeLog(entry); err != nil {
|
|
log.Error().Err(err).Msg("Failed to log profile change")
|
|
}
|
|
}
|