package handlers import ( "encoding/json" "math" "net/http" "strconv" "strings" "github.com/gin-gonic/gin" "golang.org/x/crypto/bcrypt" "git.gobha.me/xcaliber/chat-switchboard/database" "git.gobha.me/xcaliber/chat-switchboard/providers" ) // ── Types ─────────────────────────────────── type adminUserResponse struct { ID string `json:"id"` Username string `json:"username"` Email string `json:"email"` DisplayName *string `json:"display_name"` Role string `json:"role"` IsActive bool `json:"is_active"` CreatedAt string `json:"created_at"` UpdatedAt string `json:"updated_at"` LastLoginAt *string `json:"last_login_at"` Teams []userTeamMembership `json:"teams,omitempty"` } type userTeamMembership struct { TeamID string `json:"team_id"` TeamName string `json:"team_name"` Role string `json:"role"` } type adminCreateUserRequest struct { Username string `json:"username" binding:"required,min=3,max=50"` Email string `json:"email" binding:"required,email"` Password string `json:"password" binding:"required,min=8,max=128"` Role string `json:"role" binding:"required,oneof=user admin moderator"` } type adminResetPasswordRequest struct { NewPassword string `json:"new_password" binding:"required,min=8,max=128"` } type updateUserRoleRequest struct { Role string `json:"role" binding:"required,oneof=user admin moderator"` } type updateUserActiveRequest struct { IsActive bool `json:"is_active"` TeamIDs []string `json:"team_ids,omitempty"` // assign to teams on activation TeamRole string `json:"team_role,omitempty"` // role for team assignment (default: member) } type globalSettingResponse struct { Key string `json:"key"` Value map[string]interface{} `json:"value"` UpdatedAt string `json:"updated_at"` } // AdminHandler manages admin-only operations. type AdminHandler struct{} // NewAdminHandler creates a new handler. func NewAdminHandler() *AdminHandler { return &AdminHandler{} } // ── List Users ────────────────────────────── func (h *AdminHandler) ListUsers(c *gin.Context) { page, perPage, offset := parsePagination(c) var total int if err := database.DB.QueryRow(`SELECT COUNT(*) FROM users`).Scan(&total); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to count users"}) return } rows, err := database.DB.Query(` SELECT id, username, email, display_name, role, is_active, created_at, updated_at, last_login_at FROM users ORDER BY created_at ASC LIMIT $1 OFFSET $2 `, perPage, offset) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list users"}) return } defer rows.Close() users := make([]adminUserResponse, 0) for rows.Next() { var u adminUserResponse if err := rows.Scan( &u.ID, &u.Username, &u.Email, &u.DisplayName, &u.Role, &u.IsActive, &u.CreatedAt, &u.UpdatedAt, &u.LastLoginAt, ); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to scan user"}) return } users = append(users, u) } // Batch-load team memberships for all users if len(users) > 0 { userIDs := make([]interface{}, len(users)) placeholders := make([]string, len(users)) userIdx := make(map[string]int) // user_id → index in users slice for i, u := range users { userIDs[i] = u.ID placeholders[i] = "$" + strconv.Itoa(i+1) userIdx[u.ID] = i } tmRows, err := database.DB.Query(` SELECT tm.user_id, t.id, t.name, tm.role FROM team_members tm JOIN teams t ON t.id = tm.team_id WHERE tm.user_id IN (`+strings.Join(placeholders, ",")+`) ORDER BY t.name ASC `, userIDs...) if err == nil { defer tmRows.Close() for tmRows.Next() { var uid, tid, tname, trole string if tmRows.Scan(&uid, &tid, &tname, &trole) == nil { if idx, ok := userIdx[uid]; ok { users[idx].Teams = append(users[idx].Teams, userTeamMembership{ TeamID: tid, TeamName: tname, Role: trole, }) } } } } } c.JSON(http.StatusOK, paginatedResponse{ Data: users, Page: page, PerPage: perPage, Total: total, TotalPages: int(math.Ceil(float64(total) / float64(perPage))), }) } // ── Create User (admin) ───────────────────── func (h *AdminHandler) CreateUser(c *gin.Context) { var req adminCreateUserRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } req.Email = strings.ToLower(strings.TrimSpace(req.Email)) req.Username = strings.TrimSpace(req.Username) hash, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcryptCost) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to hash password"}) return } var user adminUserResponse err = database.DB.QueryRow(` INSERT INTO users (username, email, password_hash, role) VALUES ($1, $2, $3, $4) RETURNING id, username, email, display_name, role, is_active, created_at, updated_at, last_login_at `, req.Username, req.Email, string(hash), req.Role).Scan( &user.ID, &user.Username, &user.Email, &user.DisplayName, &user.Role, &user.IsActive, &user.CreatedAt, &user.UpdatedAt, &user.LastLoginAt, ) if err != nil { if strings.Contains(err.Error(), "duplicate key") { field := "email" if strings.Contains(err.Error(), "username") { field = "username" } c.JSON(http.StatusConflict, gin.H{"error": field + " already taken"}) return } c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create user"}) return } c.JSON(http.StatusCreated, user) AuditLog(c, "user.create", "user", user.ID, map[string]interface{}{ "username": req.Username, "email": req.Email, "role": req.Role, }) } // ── Reset Password (admin) ────────────────── func (h *AdminHandler) ResetPassword(c *gin.Context) { targetID := c.Param("id") var req adminResetPasswordRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } hash, err := bcrypt.GenerateFromPassword([]byte(req.NewPassword), bcryptCost) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to hash password"}) return } result, err := database.DB.Exec( `UPDATE users SET password_hash = $1, updated_at = NOW() WHERE id = $2`, string(hash), targetID, ) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to reset password"}) return } rows, _ := result.RowsAffected() if rows == 0 { c.JSON(http.StatusNotFound, gin.H{"error": "user not found"}) return } c.JSON(http.StatusOK, gin.H{"message": "password reset"}) } // ── Update User Role ──────────────────────── func (h *AdminHandler) UpdateUserRole(c *gin.Context) { targetID := c.Param("id") adminID := getUserID(c) var req updateUserRoleRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // Prevent admin from demoting themselves if targetID == adminID && req.Role != "admin" { c.JSON(http.StatusForbidden, gin.H{"error": "cannot change your own role"}) return } result, err := database.DB.Exec( `UPDATE users SET role = $1, updated_at = NOW() WHERE id = $2`, req.Role, targetID, ) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update role"}) return } rows, _ := result.RowsAffected() if rows == 0 { c.JSON(http.StatusNotFound, gin.H{"error": "user not found"}) return } c.JSON(http.StatusOK, gin.H{"message": "role updated", "role": req.Role}) AuditLog(c, "user.role_change", "user", targetID, map[string]interface{}{"role": req.Role}) } // ── Toggle User Active ────────────────────── func (h *AdminHandler) ToggleUserActive(c *gin.Context) { targetID := c.Param("id") adminID := getUserID(c) var req updateUserActiveRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // Prevent admin from deactivating themselves if targetID == adminID && !req.IsActive { c.JSON(http.StatusForbidden, gin.H{"error": "cannot deactivate your own account"}) return } result, err := database.DB.Exec( `UPDATE users SET is_active = $1, updated_at = NOW() WHERE id = $2`, req.IsActive, targetID, ) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update user"}) return } rows, _ := result.RowsAffected() if rows == 0 { c.JSON(http.StatusNotFound, gin.H{"error": "user not found"}) return } // On activation, optionally assign to teams teamsAssigned := 0 if req.IsActive && len(req.TeamIDs) > 0 { role := req.TeamRole if role == "" { role = "member" } for _, teamID := range req.TeamIDs { _, err := database.DB.Exec(` INSERT INTO team_members (team_id, user_id, role) VALUES ($1, $2, $3) ON CONFLICT (team_id, user_id) DO NOTHING `, teamID, targetID, role) if err == nil { teamsAssigned++ } } } c.JSON(http.StatusOK, gin.H{ "message": "user updated", "is_active": req.IsActive, "teams_assigned": teamsAssigned, }) action := "user.activate" if !req.IsActive { action = "user.deactivate" } AuditLog(c, action, "user", targetID, map[string]interface{}{ "is_active": req.IsActive, "teams_assigned": teamsAssigned, }) } // ── Delete User ───────────────────────────── func (h *AdminHandler) DeleteUser(c *gin.Context) { targetID := c.Param("id") adminID := getUserID(c) if targetID == adminID { c.JSON(http.StatusForbidden, gin.H{"error": "cannot delete your own account"}) return } result, err := database.DB.Exec(`DELETE FROM users WHERE id = $1`, targetID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to delete user"}) return } rows, _ := result.RowsAffected() if rows == 0 { c.JSON(http.StatusNotFound, gin.H{"error": "user not found"}) return } c.JSON(http.StatusOK, gin.H{"message": "user deleted"}) AuditLog(c, "user.delete", "user", targetID, nil) } // ── Public Settings (for any authenticated user) ── var publicSettingKeys = map[string]bool{ "banner": true, "branding": true, "user_providers_enabled": true, "registration_enabled": true, "registration_default_state": true, "banner_presets": true, } func (h *AdminHandler) PublicSettings(c *gin.Context) { rows, err := database.DB.Query(` SELECT key, value::text FROM global_settings ORDER BY key `) if err != nil { c.JSON(http.StatusOK, gin.H{"settings": []interface{}{}}) return } defer rows.Close() settings := make([]globalSettingResponse, 0) for rows.Next() { var key, valueRaw string if err := rows.Scan(&key, &valueRaw); err != nil { continue } if !publicSettingKeys[key] { continue } s := globalSettingResponse{Key: key} s.Value = make(map[string]interface{}) _ = json.Unmarshal([]byte(valueRaw), &s.Value) settings = append(settings, s) } c.JSON(http.StatusOK, gin.H{"settings": settings}) } // ── List Global Settings ──────────────────── func (h *AdminHandler) ListGlobalSettings(c *gin.Context) { rows, err := database.DB.Query(` SELECT key, value::text, updated_at FROM global_settings ORDER BY key `) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list settings"}) return } defer rows.Close() settings := make([]globalSettingResponse, 0) for rows.Next() { var s globalSettingResponse var valueRaw string if err := rows.Scan(&s.Key, &valueRaw, &s.UpdatedAt); err != nil { continue } s.Value = make(map[string]interface{}) _ = json.Unmarshal([]byte(valueRaw), &s.Value) settings = append(settings, s) } c.JSON(http.StatusOK, gin.H{"settings": settings}) } // ── Get Global Setting ────────────────────── func (h *AdminHandler) GetGlobalSetting(c *gin.Context) { key := c.Param("key") var valueRaw, updatedAt string err := database.DB.QueryRow(` SELECT value::text, updated_at FROM global_settings WHERE key = $1 `, key).Scan(&valueRaw, &updatedAt) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "setting not found"}) return } value := make(map[string]interface{}) _ = json.Unmarshal([]byte(valueRaw), &value) c.JSON(http.StatusOK, globalSettingResponse{ Key: key, Value: value, UpdatedAt: updatedAt, }) } // ── Update Global Setting ─────────────────── func (h *AdminHandler) UpdateGlobalSetting(c *gin.Context) { key := c.Param("key") adminID := getUserID(c) var value map[string]interface{} if err := c.ShouldBindJSON(&value); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } valueJSON, err := json.Marshal(value) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid value"}) return } _, err = database.DB.Exec(` INSERT INTO global_settings (key, value, updated_at, updated_by) VALUES ($1, $2::jsonb, NOW(), $3) ON CONFLICT (key) DO UPDATE SET value = $2::jsonb, updated_at = NOW(), updated_by = $3 `, key, string(valueJSON), adminID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update setting"}) return } h.GetGlobalSetting(c) } // ── Admin Stats ───────────────────────────── func (h *AdminHandler) GetStats(c *gin.Context) { stats := make(map[string]int) queries := map[string]string{ "total_users": "SELECT COUNT(*) FROM users", "active_users": "SELECT COUNT(*) FROM users WHERE is_active = true", "total_channels": "SELECT COUNT(*) FROM channels", "total_messages": "SELECT COUNT(*) FROM messages", "api_configs": "SELECT COUNT(*) FROM api_configs", } for key, query := range queries { var count int if err := database.DB.QueryRow(query).Scan(&count); err != nil { stats[key] = 0 } else { stats[key] = count } } c.JSON(http.StatusOK, stats) } // ── Global API Configs ────────────────────── type adminGlobalConfigResponse struct { ID string `json:"id"` Name string `json:"name"` Provider string `json:"provider"` Endpoint string `json:"endpoint"` ModelDefault *string `json:"model_default"` IsActive bool `json:"is_active"` IsPrivate bool `json:"is_private"` HasKey bool `json:"has_key"` CreatedAt string `json:"created_at"` } func (h *AdminHandler) ListGlobalConfigs(c *gin.Context) { rows, err := database.DB.Query(` SELECT id, name, provider, endpoint, model_default, is_active, COALESCE(is_private, false), (api_key_encrypted IS NOT NULL AND api_key_encrypted != '') as has_key, created_at FROM api_configs WHERE user_id IS NULL AND team_id IS NULL ORDER BY created_at ASC `) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list configs"}) return } defer rows.Close() configs := make([]adminGlobalConfigResponse, 0) for rows.Next() { var cfg adminGlobalConfigResponse if err := rows.Scan( &cfg.ID, &cfg.Name, &cfg.Provider, &cfg.Endpoint, &cfg.ModelDefault, &cfg.IsActive, &cfg.IsPrivate, &cfg.HasKey, &cfg.CreatedAt, ); err != nil { continue } configs = append(configs, cfg) } c.JSON(http.StatusOK, gin.H{"configs": configs}) } func (h *AdminHandler) CreateGlobalConfig(c *gin.Context) { var req createAPIConfigRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } var id string err := database.DB.QueryRow(` INSERT INTO api_configs (name, provider, endpoint, api_key_encrypted, model_default, user_id, is_global, is_private) VALUES ($1, $2, $3, $4, $5, NULL, true, $6) RETURNING id `, req.Name, req.Provider, req.Endpoint, req.APIKey, req.ModelDefault, req.IsPrivate).Scan(&id) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create config"}) return } c.JSON(http.StatusCreated, gin.H{"id": id, "message": "global config created"}) } func (h *AdminHandler) UpdateGlobalConfig(c *gin.Context) { configID := c.Param("id") var req struct { Name *string `json:"name"` Endpoint *string `json:"endpoint"` APIKey *string `json:"api_key"` ModelDefault *string `json:"model_default"` IsPrivate *bool `json:"is_private"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } sets := []string{} args := []interface{}{} argN := 1 add := func(col string, val interface{}) { sets = append(sets, col+" = $"+strconv.Itoa(argN)) args = append(args, val) argN++ } if req.Name != nil { add("name", *req.Name) } if req.Endpoint != nil { add("endpoint", *req.Endpoint) } if req.APIKey != nil && *req.APIKey != "" { add("api_key_encrypted", *req.APIKey) } if req.ModelDefault != nil { add("model_default", *req.ModelDefault) } if req.IsPrivate != nil { add("is_private", *req.IsPrivate) } if len(sets) == 0 { c.JSON(http.StatusBadRequest, gin.H{"error": "no fields to update"}) return } sets = append(sets, "updated_at = NOW()") args = append(args, configID) query := "UPDATE api_configs SET " + strings.Join(sets, ", ") + " WHERE id = $" + strconv.Itoa(argN) + " AND user_id IS NULL AND team_id IS NULL" result, err := database.DB.Exec(query, args...) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update config"}) return } rows, _ := result.RowsAffected() if rows == 0 { c.JSON(http.StatusNotFound, gin.H{"error": "global config not found"}) return } c.JSON(http.StatusOK, gin.H{"status": "updated"}) } func (h *AdminHandler) DeleteGlobalConfig(c *gin.Context) { configID := c.Param("id") result, err := database.DB.Exec( `DELETE FROM api_configs WHERE id = $1 AND user_id IS NULL AND team_id IS NULL`, configID, ) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to delete config"}) return } rows, _ := result.RowsAffected() if rows == 0 { c.JSON(http.StatusNotFound, gin.H{"error": "global config not found"}) return } c.JSON(http.StatusOK, gin.H{"message": "global config deleted"}) } // ── Model Configs ─────────────────────────── type modelConfigResponse struct { ID string `json:"id"` APIConfigID string `json:"api_config_id"` ProviderName string `json:"provider_name"` ModelID string `json:"model_id"` DisplayName *string `json:"display_name"` Visibility string `json:"visibility"` Capabilities map[string]interface{} `json:"capabilities"` CreatedAt string `json:"created_at"` UpdatedAt string `json:"updated_at"` } type updateModelConfigRequest struct { Visibility *string `json:"visibility"` DisplayName *string `json:"display_name"` Capabilities map[string]interface{} `json:"capabilities"` } func (h *AdminHandler) ListModelConfigs(c *gin.Context) { rows, err := database.DB.Query(` SELECT mc.id, mc.api_config_id, ac.name, mc.model_id, mc.display_name, mc.visibility, mc.capabilities, mc.created_at, mc.updated_at FROM model_configs mc JOIN api_configs ac ON mc.api_config_id = ac.id WHERE ac.user_id IS NULL AND ac.team_id IS NULL ORDER BY ac.name, mc.model_id `) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list models"}) return } defer rows.Close() models := make([]modelConfigResponse, 0) for rows.Next() { var m modelConfigResponse var capsJSON []byte if err := rows.Scan( &m.ID, &m.APIConfigID, &m.ProviderName, &m.ModelID, &m.DisplayName, &m.Visibility, &capsJSON, &m.CreatedAt, &m.UpdatedAt, ); err != nil { continue } _ = json.Unmarshal(capsJSON, &m.Capabilities) models = append(models, m) } c.JSON(http.StatusOK, gin.H{"models": models}) } func (h *AdminHandler) FetchModels(c *gin.Context) { // Load all global api_configs rows, err := database.DB.Query(` SELECT id, provider, endpoint, api_key_encrypted, custom_headers FROM api_configs WHERE user_id IS NULL AND team_id IS NULL AND is_active = true `) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list configs"}) return } defer rows.Close() type fetchResult struct { ConfigID string `json:"config_id"` Provider string `json:"provider"` Added int `json:"added"` Updated int `json:"updated"` Skipped int `json:"skipped"` Error string `json:"error,omitempty"` } results := make([]fetchResult, 0) totalAdded := 0 for rows.Next() { var cfgID, providerID, endpoint string var apiKey *string var customHeadersJSON []byte if err := rows.Scan(&cfgID, &providerID, &endpoint, &apiKey, &customHeadersJSON); err != nil { continue } prov, err := providers.Get(providerID) if err != nil { results = append(results, fetchResult{ConfigID: cfgID, Provider: providerID, Error: err.Error()}) continue } key := "" if apiKey != nil { key = *apiKey } customHeaders := make(map[string]string) if customHeadersJSON != nil { _ = json.Unmarshal(customHeadersJSON, &customHeaders) } models, err := prov.ListModels(c.Request.Context(), providers.ProviderConfig{ Endpoint: endpoint, APIKey: key, CustomHeaders: customHeaders, }) if err != nil { results = append(results, fetchResult{ConfigID: cfgID, Provider: providerID, Error: err.Error()}) continue } fr := fetchResult{ConfigID: cfgID, Provider: providerID} for _, m := range models { // Serialize capabilities to JSONB capsJSON, _ := json.Marshal(m.Capabilities) result, err := database.DB.Exec(` INSERT INTO model_configs (api_config_id, model_id, display_name, capabilities) VALUES ($1, $2, $3, $4) ON CONFLICT (api_config_id, model_id) DO UPDATE SET display_name = COALESCE(NULLIF(model_configs.display_name, ''), EXCLUDED.display_name), capabilities = EXCLUDED.capabilities, updated_at = NOW() `, cfgID, m.ID, m.Name, capsJSON) if err != nil { fr.Skipped++ continue } affected, _ := result.RowsAffected() if affected > 0 { fr.Added++ totalAdded++ } else { fr.Skipped++ } } results = append(results, fr) } c.JSON(http.StatusOK, gin.H{ "total_added": totalAdded, "results": results, }) } func (h *AdminHandler) UpdateModelConfig(c *gin.Context) { modelID := c.Param("id") var req updateModelConfigRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // Build dynamic update if req.Visibility != nil { v := *req.Visibility if v != "enabled" && v != "disabled" && v != "team" { c.JSON(http.StatusBadRequest, gin.H{"error": "visibility must be enabled, disabled, or team"}) return } _, err := database.DB.Exec( `UPDATE model_configs SET visibility = $1, updated_at = NOW() WHERE id = $2`, v, modelID, ) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update visibility"}) return } } if req.DisplayName != nil { _, err := database.DB.Exec( `UPDATE model_configs SET display_name = $1, updated_at = NOW() WHERE id = $2`, *req.DisplayName, modelID, ) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update name"}) return } } if req.Capabilities != nil { capsJSON, err := json.Marshal(req.Capabilities) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid capabilities"}) return } _, err = database.DB.Exec( `UPDATE model_configs SET capabilities = $1, updated_at = NOW() WHERE id = $2`, string(capsJSON), modelID, ) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update capabilities"}) return } } c.JSON(http.StatusOK, gin.H{"message": "model updated"}) } // BulkUpdateModels sets visibility for all models at once func (h *AdminHandler) BulkUpdateModels(c *gin.Context) { var req struct { Visibility string `json:"visibility"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if req.Visibility != "enabled" && req.Visibility != "disabled" && req.Visibility != "team" { c.JSON(http.StatusBadRequest, gin.H{"error": "visibility must be enabled, disabled, or team"}) return } result, err := database.DB.Exec( `UPDATE model_configs SET visibility = $1, updated_at = NOW() WHERE api_config_id IN (SELECT id FROM api_configs WHERE user_id IS NULL AND team_id IS NULL)`, req.Visibility, ) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to bulk update models"}) return } rows, _ := result.RowsAffected() c.JSON(http.StatusOK, gin.H{"message": "models updated", "count": rows}) } func (h *AdminHandler) DeleteModelConfig(c *gin.Context) { modelID := c.Param("id") result, err := database.DB.Exec(`DELETE FROM model_configs WHERE id = $1`, modelID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to delete model"}) return } rows, _ := result.RowsAffected() if rows == 0 { c.JSON(http.StatusNotFound, gin.H{"error": "model not found"}) return } c.JSON(http.StatusOK, gin.H{"message": "model deleted"}) }