Files
Containr/app/backend/internal/api/exec.go
T
Tomas DvorakandDevin 8b6214a0f5 feat: public platform, admin console, templates, landing docs
Projects become publicly browsable after admin approval; management and
secret-adjacent reads (logs, variables, exec, connection URLs) require
owner/member/admin auth, and platform controls sit behind RequireAdmin.

- Backend: OptionalAuth middleware, read/authed/admin route groups,
  projects.is_approved + service_templates.owner_id migration,
  visibility-aware sqlc queries, admin overview/users/approval endpoints,
  EnsureAdminAccount bootstrap (ADMIN_* env), 30 seeded templates with
  owner CRUD, managed-database connection URLs gated owner+admin, fix
  DB.Stats() infinite recursion.
- Frontend: expandable persisted sidebar + clickable user menu, public
  landing page with searchable docs browser (GitHub sync, bundled
  snapshot + localStorage fallback), DocsBrowser shared with /docs,
  template create/edit/JSON-upload for owned templates, database create
  dialog, admin console (stats, approvals, project edit/delete, user
  admin toggles), HA/Security/Usage polish with dead CSS tokens mapped
  to real theme vars, anonymous-safe gating on every mutation control.
- Tests: safe window.localStorage access fixes happy-dom crashes in
  PlatformShell and canvas storage.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-22 19:58:59 +02:00

83 lines
2.1 KiB
Go

package api
import (
"context"
"net/http"
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"containr/internal/database"
"containr/internal/docker"
)
// handleExecInService runs a one-off shell command inside the service's
// running container and returns captured output. Intended for debugging;
// commands run as the container's default user with a 30s ceiling.
func handleExecInService(c *gin.Context) {
serviceID := c.Param("id")
db := c.MustGet("db").(*database.DB)
var req struct {
Command string `json:"command" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
command := strings.TrimSpace(req.Command)
if command == "" || len(command) > 4096 {
c.JSON(http.StatusBadRequest, gin.H{"error": "command must be 1-4096 chars"})
return
}
serviceUUID, err := uuid.Parse(serviceID)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid service ID"})
return
}
projectID, found := serviceProjectID(db, serviceUUID)
if !found {
c.JSON(http.StatusNotFound, gin.H{"error": "Service not found"})
return
}
if _, allowed := projectManageAccess(c, db, projectID); !allowed {
c.JSON(http.StatusForbidden, gin.H{"error": "Access denied"})
return
}
dockerClient, _ := c.Get("docker_client")
dockerCli, _ := dockerClient.(*docker.Client)
if dockerCli == nil {
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "Docker unavailable"})
return
}
ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second)
defer cancel()
containerID, err := findServiceContainer(ctx, dockerCli, serviceID)
if err != nil {
c.JSON(http.StatusConflict, gin.H{"error": "No running container for this service"})
return
}
output, exitCode, runErr := dockerCli.ExecRun(ctx, containerID, []string{"sh", "-c", command})
if len(output) > 64*1024 {
output = output[:64*1024]
}
errText := ""
if runErr != nil {
errText = runErr.Error()
}
c.JSON(http.StatusOK, gin.H{
"output": output,
"exit_code": exitCode,
"error": errText,
})
}