From e00a0da5d92f2f0fbf6dee6d79585e6a5221630a Mon Sep 17 00:00:00 2001 From: mrhid6 Date: Tue, 4 Aug 2026 13:34:22 +0100 Subject: [PATCH] feat: tag endpoints for servers --- server/internal/api/handlers.go | 52 ++++++++++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/server/internal/api/handlers.go b/server/internal/api/handlers.go index 55f1337..3dc48da 100644 --- a/server/internal/api/handlers.go +++ b/server/internal/api/handlers.go @@ -51,6 +51,9 @@ func RegisterRoutes(r *gin.Engine) { apiGroup.POST("/license", auth.RequireRole("owner"), postLicence) apiGroup.GET("/servers", listServers) + // Static segment, registered alongside /servers/:id exactly as + // /servers/new already is — gin resolves statics ahead of wildcards. + apiGroup.GET("/servers/tags", listKnownTags) apiGroup.POST("/servers", createServer) apiGroup.GET("/servers/new", newServer) apiGroup.POST("/servers/new", newServer) @@ -59,6 +62,7 @@ func RegisterRoutes(r *gin.Engine) { apiGroup.POST("/servers/:id/generate-key", generateKey) apiGroup.POST("/servers/:id/update-agent", updateAgent) apiGroup.POST("/servers/:id/apply-updates", applyUpdates) + apiGroup.PUT("/servers/:id/tags", putServerTags) apiGroup.GET("/agent/latest-version", getLatestAgentVersion) @@ -119,7 +123,12 @@ func RegisterRoutes(r *gin.Engine) { } func listServers(c *gin.Context) { - servers, err := services.ListServers(auth.InstanceID(c)) + sel, err := services.ParseTagFilters(c.QueryArray("tag")) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + servers, err := services.ListServersFiltered(auth.InstanceID(c), sel) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return @@ -127,6 +136,47 @@ func listServers(c *gin.Context) { c.JSON(http.StatusOK, servers) } +func listKnownTags(c *gin.Context) { + tags, err := services.KnownTags(auth.InstanceID(c)) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, tags) +} + +func putServerTags(c *gin.Context) { + var body struct { + Tags map[string]string `json:"tags"` + } + if err := c.ShouldBindJSON(&body); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid body"}) + return + } + + instanceID := auth.InstanceID(c) + serverID := c.Param("id") + + before, err := services.GetServer(instanceID, serverID) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "server not found"}) + return + } + + if err := services.SetServerTags(instanceID, serverID, body.Tags); err != nil { + if errors.Is(err, services.ErrInvalidTag) { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + services.LogEvent(instanceID, "server.tags_updated", actorFromCtx(c), serverID, "", + fmt.Sprintf("tags %v -> %v", before.Tags, body.Tags)) + c.JSON(http.StatusOK, gin.H{"tags": body.Tags}) +} + func createServer(c *gin.Context) { s, token, err := services.CreateServer(auth.InstanceID(c)) if err != nil {