package v1 import ( "encoding/json" "errors" "net/http" "github.com/gin-gonic/gin" "github.com/gochat/gochat/internal/llm" "github.com/gochat/gochat/internal/model" "github.com/gochat/gochat/internal/service" applogger "github.com/gochat/gochat/pkg/logger" "github.com/gochat/gochat/pkg/response" ) // CopilotConfigHandler exposes typed platform and account configuration APIs. // Platform mutations are registered behind middleware.SuperAdmin; account // configuration remains administrator scoped. type CopilotConfigHandler struct { platform *service.CopilotConfigService account *service.CaptainPreferenceService audit *service.AuditService articles *service.ArticleService } func NewCopilotConfigHandler(platform *service.CopilotConfigService, account *service.CaptainPreferenceService) *CopilotConfigHandler { return &CopilotConfigHandler{platform: platform, account: account} } func (h *CopilotConfigHandler) WithAuditService(audit *service.AuditService) *CopilotConfigHandler { h.audit = audit return h } func (h *CopilotConfigHandler) WithArticleService(articles *service.ArticleService) *CopilotConfigHandler { h.articles = articles return h } func (h *CopilotConfigHandler) PlatformGet(c *gin.Context) { payload, err := h.platform.Get(c.Request.Context()) if err != nil { handleServiceError(c, err) return } c.JSON(http.StatusOK, payload) } func (h *CopilotConfigHandler) PlatformUpdate(c *gin.Context) { var input service.CopilotProviderConfigInput if err := c.ShouldBindJSON(&input); err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error()) return } payload, err := h.platform.Update(c.Request.Context(), input) if err != nil { c.JSON(http.StatusUnprocessableEntity, gin.H{"error": err.Error()}) return } h.recordPlatformUpdate(c, input, payload) c.JSON(http.StatusOK, payload) } func (h *CopilotConfigHandler) recordPlatformUpdate(c *gin.Context, input service.CopilotProviderConfigInput, payload *service.CopilotProviderConfigPayload) { if h.audit == nil || payload == nil { return } changes := map[string]any{ "chat": map[string]any{ "provider": payload.Chat.Provider, "base_url": payload.Chat.BaseURL, "model": payload.Chat.Model, "api_key_configured": payload.Chat.APIKey.Configured, "api_key_changed": input.Chat.APIKey != "" || input.Chat.ClearAPIKey, }, "embedding": map[string]any{ "mode": payload.Embedding.Mode, "provider": payload.Embedding.Provider, "base_url": payload.Embedding.BaseURL, "model": payload.Embedding.Model, "dimensions": payload.Embedding.Dimensions, "api_key_configured": payload.Embedding.APIKey.Configured, "api_key_changed": input.Embedding.APIKey != "" || input.Embedding.ClearAPIKey, }, "generation": payload.Generation, "request": payload.Request, "configured": payload.Configured, } raw, err := json.Marshal(changes) if err != nil { return } audit := &model.Audit{ AuditableType: "InstallationConfig", AuditableID: 1, Action: "update", AuditedChanges: raw, RemoteAddress: c.ClientIP(), RequestUUID: firstNonEmpty(c.GetHeader("X-Request-ID"), c.GetHeader("X-Correlation-ID")), Comment: "Copilot provider configuration updated", } if accountID := c.GetUint("account_id"); accountID != 0 { audit.AccountID = &accountID audit.AssociatedType = "Account" audit.AssociatedID = &accountID } if userID := getUserID(c); userID != 0 { audit.UserID = &userID audit.UserType = "SuperAdmin" } if _, err := h.audit.CreateAudit(c.Request.Context(), audit); err != nil { applogger.L().Warnf("Copilot provider audit skipped: %v", err) } } func (h *CopilotConfigHandler) PlatformTest(c *gin.Context) { var input service.CopilotProviderConfigInput if err := c.ShouldBindJSON(&input); err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error()) return } payload, err := h.platform.Test(c.Request.Context(), input) if err != nil { status := http.StatusUnprocessableEntity if errors.Is(err, llm.ErrProviderNotConfigured) { response.AbortWithStatusError(c, http.StatusConflict, response.ErrCopilotNotConfigured, err.Error()) return } c.JSON(status, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, payload) } func (h *CopilotConfigHandler) PlatformEmbeddingReindexStatus(c *gin.Context) { if h.articles == nil { response.AbortWithStatusError(c, http.StatusServiceUnavailable, response.ErrServiceUnavail, "embedding reindex is not configured") return } c.JSON(http.StatusOK, h.articles.EmbeddingReindexStatus()) } func (h *CopilotConfigHandler) PlatformEmbeddingReindexStart(c *gin.Context) { if h.articles == nil { response.AbortWithStatusError(c, http.StatusServiceUnavailable, response.ErrServiceUnavail, "embedding reindex is not configured") return } status, err := h.articles.StartEmbeddingReindex() if err != nil { code := http.StatusUnprocessableEntity if status.Running { code = http.StatusConflict } c.JSON(code, gin.H{"error": err.Error(), "status": status}) return } c.JSON(http.StatusAccepted, status) } func (h *CopilotConfigHandler) AccountGet(c *gin.Context) { if !captainPreferencesCanUpdate(c) { response.AbortWithStatusError(c, http.StatusForbidden, response.ErrForbidden, "administrator role required") return } accountID := parseAccountIDParam(c) if accountID == 0 { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id") return } payload, err := h.account.GetConfig(c.Request.Context(), accountID) if err != nil { handleServiceError(c, err) return } c.JSON(http.StatusOK, payload) } func (h *CopilotConfigHandler) AccountUpdate(c *gin.Context) { if !captainPreferencesCanUpdate(c) { response.AbortWithStatusError(c, http.StatusForbidden, response.ErrForbidden, "administrator role required") return } accountID := parseAccountIDParam(c) if accountID == 0 { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id") return } var input service.UpdateCaptainConfigRequest if err := c.ShouldBindJSON(&input); err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error()) return } payload, err := h.account.UpdateConfig(c.Request.Context(), accountID, &input) if err != nil { c.JSON(http.StatusUnprocessableEntity, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, payload) }