package handlers // team_package_settings.go // // Team-admin endpoints for managing team-scoped package settings. // These sit in the settings cascade between global (admin) and user. // Only overridable keys (user_overridable != false) can be set at team scope. import ( "encoding/json" "strings" "github.com/gin-gonic/gin" "armature/store" ) // TeamPackageSettingsHandler serves team-scoped package settings endpoints. type TeamPackageSettingsHandler struct { stores store.Stores } func NewTeamPackageSettingsHandler(s store.Stores) *TeamPackageSettingsHandler { return &TeamPackageSettingsHandler{stores: s} } // GetTeamPackageSettings returns the team-scoped settings for a package, // along with the overridable schema keys for the UI. // GET /api/v1/teams/:teamId/packages/:id/settings func (h *TeamPackageSettingsHandler) GetTeamPackageSettings(c *gin.Context) { teamID := c.Param("teamId") pkgID := c.Param("id") pkg, err := h.stores.Packages.Get(c.Request.Context(), pkgID) if err != nil || pkg == nil { c.JSON(404, gin.H{"error": "package not found"}) return } // Parse schema and filter to overridable keys only schema := store.ParseSettingsSchema(pkg.Manifest) var overridableSchema []store.SettingSchema for _, s := range schema { if s.IsUserOverridable() { overridableSchema = append(overridableSchema, s) } } // Load current team values settings, err := h.stores.Packages.GetTeamSettings(c.Request.Context(), pkgID, teamID) if err != nil { settings = json.RawMessage("{}") } c.JSON(200, gin.H{ "schema": overridableSchema, "values": json.RawMessage(settings), }) } // UpdateTeamPackageSettings saves team-scoped overrides for a package. // Non-overridable keys are stripped before saving. // PUT /api/v1/teams/:teamId/packages/:id/settings func (h *TeamPackageSettingsHandler) UpdateTeamPackageSettings(c *gin.Context) { teamID := c.Param("teamId") pkgID := c.Param("id") pkg, err := h.stores.Packages.Get(c.Request.Context(), pkgID) if err != nil || pkg == nil { c.JSON(404, gin.H{"error": "package not found"}) return } var body struct { Settings json.RawMessage `json:"settings" binding:"required"` } if err := c.ShouldBindJSON(&body); err != nil { c.JSON(400, gin.H{"error": "invalid request body"}) return } // Parse and enforce user_overridable schema := store.ParseSettingsSchema(pkg.Manifest) var incoming map[string]any if err := json.Unmarshal(body.Settings, &incoming); err != nil { c.JSON(400, gin.H{"error": "settings must be a JSON object"}) return } filtered, rejected := store.FilterOverridableKeys(incoming, schema) if len(filtered) == 0 && len(rejected) > 0 { c.JSON(400, gin.H{"error": "these settings cannot be overridden: " + strings.Join(rejected, ", ")}) return } settingsJSON, err := json.Marshal(filtered) if err != nil { c.JSON(500, gin.H{"error": "failed to marshal settings"}) return } if err := h.stores.Packages.SetTeamSettings(c.Request.Context(), pkgID, teamID, json.RawMessage(settingsJSON)); err != nil { c.JSON(500, gin.H{"error": "failed to save team settings"}) return } c.JSON(200, gin.H{"ok": true, "rejected_keys": rejected}) } // DeleteTeamPackageSettings removes team-scoped overrides for a package. // DELETE /api/v1/teams/:teamId/packages/:id/settings func (h *TeamPackageSettingsHandler) DeleteTeamPackageSettings(c *gin.Context) { teamID := c.Param("teamId") pkgID := c.Param("id") if err := h.stores.Packages.DeleteTeamSettings(c.Request.Context(), pkgID, teamID); err != nil { c.JSON(500, gin.H{"error": "failed to delete team settings"}) return } c.JSON(200, gin.H{"ok": true}) }