mirror of
https://github.com/Dvorinka/Containr.git
synced 2026-10-28 07:41:27 +00:00
Per-user notification_channels (ntfy + Gotify): every
insertUserNotification now fans out best-effort to enabled channels —
deployment/backup/security/agent alerts reach the user's phone via the
ntfy app with zero extra infrastructure.
- POST/GET/DELETE /notifications/channels + POST .../:id/test which
pushes a real probe so misconfiguration surfaces before alerts depend
on it (502 DELIVERY_FAILED on endpoint errors).
- ntfy: POST to endpoint (topic in path), Title/Tags headers, optional
Bearer token. Gotify: POST {endpoint}/message?token=...
- Delivery is best-effort by design — a dead relay never breaks the
producing flow; one shared 5s-timeout client.
- CLI `notifications channels list|add|test|remove`, MCP tools,
OpenAPI paths + NotificationChannel schema.
- Web settings "Push notifications" section and a Push channels card on
the mobile notifications tab (add/test/remove).
Verified live: channel CRUD, test-ping hit a recording endpoint with
correct headers/body, and a real deployment fanned out "Deployment
succeeded" through the channel end-to-end.
Generated with [Devin](https://devin.ai)
Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
578 lines
24 KiB
Go
578 lines
24 KiB
Go
package api
|
|
|
|
import (
|
|
"context"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
"containr/internal/build"
|
|
"containr/internal/config"
|
|
"containr/internal/database"
|
|
"containr/internal/deployment"
|
|
"containr/internal/deployqueue"
|
|
"containr/internal/docker"
|
|
"containr/internal/ha"
|
|
"containr/internal/metrics"
|
|
"containr/internal/middleware"
|
|
"containr/internal/scaling"
|
|
"containr/internal/secrets"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
func SetupRoutes(router *gin.Engine, db *database.DB, redis *database.Redis, cfg *config.Config) {
|
|
// Audit + outbound webhook delivery need a package-level handle for
|
|
// call sites that don't carry a gin context.
|
|
SetAuditDB(db)
|
|
StartWebhookDispatcher()
|
|
|
|
// Expose Better Auth through backend so frontend can use a single backend origin.
|
|
setupAuthProxyRoutes(router, cfg, db)
|
|
|
|
// Initialize Docker client (non-fatal if it fails)
|
|
var dockerClient *docker.Client
|
|
var buildManager *build.BuildManager
|
|
var deploymentEngine *deployment.DeploymentEngine
|
|
|
|
if client, err := docker.NewClient(); err != nil {
|
|
log.Printf("Warning: Failed to initialize Docker client: %v", err)
|
|
log.Printf("Docker-related features will be disabled")
|
|
} else {
|
|
dockerClient = client
|
|
buildManager = build.NewBuildManager("/tmp/containr-builds", dockerClient)
|
|
deploymentEngine = deployment.NewDeploymentEngine(buildManager, dockerClient)
|
|
initArtifacts(filepath.Join("/tmp/containr-builds", "artifacts"))
|
|
purgeStaleArtifacts()
|
|
// Node-pinned services dispatch container lifecycle to the agent's
|
|
// command queue instead of the local Docker socket.
|
|
deploymentEngine.SetNodeRunner(newAgentNodeRunner(db))
|
|
// Restore managed sidecars (e.g. cloudflared) from saved settings.
|
|
syncManagedContainers(dockerClient, db)
|
|
}
|
|
|
|
// Initialize build handler
|
|
buildHandler := NewBuildHandler(buildManager, dockerClient, db)
|
|
|
|
// Initialize scheduler and metrics systems
|
|
scheduler := deployment.NewScheduler()
|
|
var metricsStorage metrics.MetricsStorage = metrics.NewInMemoryMetricsStorage()
|
|
if db != nil && db.DB != nil {
|
|
metricsStorage = metrics.NewPostgreSQLMetricsStorage(db.DB)
|
|
}
|
|
metricsCollector := metrics.NewMetricsCollector(scheduler, metricsStorage)
|
|
autoScaler := scaling.NewAutoScaler(scheduler, metricsCollector)
|
|
if db != nil && db.DB != nil {
|
|
autoScaler.WithPersistence(db.DB)
|
|
if err := autoScaler.LoadPolicies(context.Background()); err != nil {
|
|
log.Printf("Failed to restore scaling policies: %v", err)
|
|
}
|
|
}
|
|
haManager := ha.NewHighAvailabilityManager(scheduler, metricsCollector)
|
|
if db != nil && db.DB != nil {
|
|
haManager.WithPersistence(db.DB)
|
|
if err := haManager.LoadHealthChecks(context.Background()); err != nil {
|
|
log.Printf("Failed to restore health checks: %v", err)
|
|
}
|
|
}
|
|
if dockerClient != nil && db != nil {
|
|
// Service-bound checks probe the service DNS name on the project
|
|
// network — the backend container joins it on demand. The old
|
|
// 127.0.0.1 fallback stays for node-bound checks.
|
|
docker := dockerClient
|
|
haManager.WithServiceResolver(func(ctx context.Context, serviceID string) (string, int, error) {
|
|
var name, projectID string
|
|
var port int
|
|
err := db.QueryRowContext(ctx,
|
|
`SELECT name, project_id::text, COALESCE(port, 0) FROM services WHERE id = $1`,
|
|
serviceID,
|
|
).Scan(&name, &projectID, &port)
|
|
if err != nil {
|
|
return "", 0, err
|
|
}
|
|
if err := docker.ConnectSelfToNetwork(ctx, deployment.ProjectNetworkName(projectID)); err != nil {
|
|
return "", 0, err
|
|
}
|
|
return name, port, nil
|
|
})
|
|
}
|
|
haAPIManager := NewHAManager(haManager)
|
|
|
|
// Initialize scaling handler
|
|
scalingHandler := NewScalingHandler(autoScaler)
|
|
|
|
// Initialize agent handler (sqlc-backed)
|
|
agentHandler := NewNodeAgentHandler(db)
|
|
|
|
// Start the cron scheduler — executes due cron_jobs via docker exec.
|
|
StartCronScheduler(context.Background(), db, dockerClient)
|
|
|
|
// Sleep sweeper — scales idle services to zero and prunes wake containers.
|
|
StartSleepSweeper(context.Background(), db, dockerClient, deploymentEngine, cfg.Port)
|
|
StartPreviewSweeper(context.Background(), db, deploymentEngine)
|
|
|
|
// Audit retention — prunes rows past AUDIT_RETENTION_DAYS (default 90).
|
|
StartAuditRetention(context.Background(), db)
|
|
|
|
// Agent health — marks stale heartbeats offline, notifies admins on
|
|
// offline/recovery transitions.
|
|
StartAgentHealthSweep(context.Background(), db)
|
|
|
|
// Daily upgrade check — notifies admins when a newer release exists.
|
|
StartUpgradeCheck(context.Background(), db, Version)
|
|
|
|
// Initialize database handler
|
|
databaseHandler := NewDatabaseHandler(db.DB, dockerClient)
|
|
|
|
// Per-service serialized deploy queue — one active deployment per
|
|
// service, the rest wait as 'queued'.
|
|
deployQueue := deployqueue.New()
|
|
|
|
// Encrypt-at-rest for secret env vars. SECRETS_KEY wins, JWT_SECRET is
|
|
// the fallback so existing installs need no new config.
|
|
secrets.Configure(os.Getenv("SECRETS_KEY"), cfg.JWTSecret)
|
|
|
|
// Boot sweep: in-memory engine state dies on restart, so anything that
|
|
// looked in-flight is actually orphaned — mark it failed rather than
|
|
// leaving phantom 'building'/'queued' rows.
|
|
_, _ = db.Exec(
|
|
`UPDATE deployments SET status = 'failed', error = 'Interrupted by server restart', completed_at = NOW(), updated_at = NOW()
|
|
WHERE status IN ('queued', 'pending', 'building', 'deploying', 'rolling_back')`,
|
|
)
|
|
|
|
// Boot sweep: encrypt any plaintext secret values left over from before
|
|
// encryption-at-rest landed. Transparent and idempotent.
|
|
if encRows, err := db.Query(`SELECT id, value FROM environment_variables WHERE is_secret = TRUE`); err == nil {
|
|
type encRow struct {
|
|
id string
|
|
value string
|
|
}
|
|
var toEncrypt []encRow
|
|
for encRows.Next() {
|
|
var r encRow
|
|
if err := encRows.Scan(&r.id, &r.value); err == nil && !secrets.IsEncrypted(r.value) {
|
|
toEncrypt = append(toEncrypt, r)
|
|
}
|
|
}
|
|
encRows.Close()
|
|
for _, r := range toEncrypt {
|
|
_, _ = db.Exec(`UPDATE environment_variables SET value = $1, updated_at = NOW() WHERE id = $2`, secrets.Encrypt(r.value), r.id)
|
|
}
|
|
if len(toEncrypt) > 0 {
|
|
log.Printf("secrets: encrypted %d plaintext secret variable(s)", len(toEncrypt))
|
|
}
|
|
}
|
|
|
|
// Initialize security handler
|
|
securityHandler := NewSecurityHandler(db, cfg.JWTSecret)
|
|
|
|
// Note: Proxmox integration can be added later if needed
|
|
// For now, focusing on core Containr and APwhy functionality
|
|
|
|
// Add database and JWT secret to gin context for handlers
|
|
router.Use(func(c *gin.Context) {
|
|
c.Set("db", db)
|
|
c.Set("redis", redis)
|
|
c.Set("jwt_secret", cfg.JWTSecret)
|
|
c.Set("docker_client", dockerClient)
|
|
c.Set("database_handler", databaseHandler)
|
|
c.Set("build_manager", buildManager)
|
|
if deploymentEngine != nil {
|
|
c.Set("deployment_engine", deploymentEngine)
|
|
}
|
|
c.Set("deploy_queue", deployQueue)
|
|
c.Set("api_port", cfg.Port)
|
|
c.Set("scheduler", scheduler)
|
|
c.Set("metrics_collector", metricsCollector)
|
|
c.Set("auto_scaler", autoScaler)
|
|
c.Set("ha_manager", haManager)
|
|
c.Set("scaling_handler", scalingHandler)
|
|
c.Next()
|
|
})
|
|
|
|
// Seed the official service template catalog (idempotent upserts).
|
|
go SeedOfficialTemplates(context.Background(), db)
|
|
|
|
go func() {
|
|
if err := haManager.Start(context.Background()); err != nil {
|
|
log.Printf("HA manager exited: %v", err)
|
|
}
|
|
}()
|
|
|
|
// Health check endpoint
|
|
router.GET("/live", func(c *gin.Context) {
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"status": "ok",
|
|
"service": "containr-api",
|
|
})
|
|
})
|
|
router.HEAD("/live", func(c *gin.Context) {
|
|
c.Status(http.StatusOK)
|
|
})
|
|
|
|
healthHandler := func(c *gin.Context) {
|
|
ctx, cancel := context.WithTimeout(c.Request.Context(), 2*time.Second)
|
|
defer cancel()
|
|
|
|
databaseStatus := "ok"
|
|
redisStatus := "ok"
|
|
checks := gin.H{
|
|
"database": databaseStatus,
|
|
"redis": redisStatus,
|
|
}
|
|
overallStatus := "ok"
|
|
statusCode := http.StatusOK
|
|
|
|
if err := db.Health(ctx); err != nil {
|
|
databaseStatus = "unhealthy"
|
|
checks["database"] = databaseStatus
|
|
checks["databaseError"] = err.Error()
|
|
overallStatus = "degraded"
|
|
statusCode = http.StatusServiceUnavailable
|
|
}
|
|
|
|
if redis == nil {
|
|
redisStatus = "unhealthy"
|
|
checks["redis"] = "unhealthy"
|
|
checks["redisError"] = "redis client not initialized"
|
|
overallStatus = "degraded"
|
|
statusCode = http.StatusServiceUnavailable
|
|
} else if err := redis.Health(ctx); err != nil {
|
|
redisStatus = "unhealthy"
|
|
checks["redis"] = redisStatus
|
|
checks["redisError"] = err.Error()
|
|
overallStatus = "degraded"
|
|
statusCode = http.StatusServiceUnavailable
|
|
}
|
|
|
|
c.JSON(statusCode, gin.H{
|
|
"status": overallStatus,
|
|
"service": "containr-api",
|
|
"checks": checks,
|
|
})
|
|
}
|
|
router.GET("/health", healthHandler)
|
|
router.HEAD("/health", healthHandler)
|
|
router.GET("/ready", healthHandler)
|
|
router.HEAD("/ready", healthHandler)
|
|
|
|
// API v1 routes
|
|
publicAgents := router.Group("/api")
|
|
agentHandler.SetupPublicRoutes(publicAgents)
|
|
|
|
// Public push receiver: HMAC signature on git_webhooks.webhook_secret
|
|
// replaces session auth. Providers POST to /api/git/webhooks/:id.
|
|
router.POST("/api/git/webhooks/:id", handleGitWebhookPush)
|
|
|
|
// GitHub App-level webhook: installed repos deliver to this single
|
|
// endpoint, verified against the provisioned webhook secret.
|
|
router.POST("/api/git/github-app/webhook", handleGitHubAppWebhook)
|
|
|
|
// Public API gateway data plane. Lives outside /api/v1 so each service's
|
|
// route_prefix (/g/<slug>) maps cleanly; auth is the caller's api_key,
|
|
// not a session.
|
|
router.Any("/g/:slug", handleGatewayProxy)
|
|
router.Any("/g/:slug/*path", handleGatewayProxy)
|
|
|
|
v1 := router.Group("/api/v1")
|
|
{
|
|
// Public routes (no authentication required)
|
|
public := v1.Group("/")
|
|
{
|
|
public.GET("/auth/bootstrap", handleAuthBootstrap)
|
|
public.POST("/auth/login", handleLogin)
|
|
public.POST("/auth/register", handleRegister)
|
|
public.GET("/maintenance", handleMaintenancePage)
|
|
public.GET("/branding", handleGetBranding)
|
|
|
|
// Invite accept flow — validates the link, then registers
|
|
// even while public signup is closed.
|
|
public.GET("/auth/invites/:token", handleGetInvite)
|
|
public.POST("/auth/accept-invite", handleAcceptInvite)
|
|
|
|
// Wake callback for sleep-mode placeholders. Unauthenticated but
|
|
// non-destructive: it only reconciles the service back to running.
|
|
public.Any("/internal/wake/:id", handleServiceWake)
|
|
public.Any("/internal/wake-page/:id", handleServiceWakePage)
|
|
}
|
|
|
|
// Read routes — public browsing. OptionalAuth resolves a session when
|
|
// present so handlers can widen visibility for owners/admins; anonymous
|
|
// callers see approved resources only.
|
|
read := v1.Group("/")
|
|
read.Use(middleware.OptionalAuth(cfg.JWTSecret))
|
|
{
|
|
read.GET("/projects", handleGetProjects)
|
|
read.GET("/projects/:id", handleGetProject)
|
|
read.GET("/projects/:id/services", handleGetServices)
|
|
read.GET("/projects/:id/environments", handleListEnvironments)
|
|
read.GET("/projects/:id/preview-environments", handleGetPreviewEnvironments)
|
|
read.GET("/projects/:id/security/history", securityHandler.GetProjectSecurityHistory)
|
|
read.GET("/projects/:id/vulnerabilities", securityHandler.GetVulnerabilities)
|
|
read.GET("/projects/:id/security/metrics", securityHandler.GetSecurityMetrics)
|
|
|
|
read.GET("/services/:id", handleGetService)
|
|
read.GET("/services/:id/metrics", handleGetServiceMetrics)
|
|
read.GET("/services/:id/runtime", handleGetServiceRuntime)
|
|
read.GET("/services/:id/deployments", handleGetDeployments)
|
|
read.GET("/services/:id/domains", handleListServiceDomains)
|
|
read.GET("/services/:id/domains/check", handleCheckServiceDomains)
|
|
read.GET("/services/:id/env-check", handleServiceEnvCheck)
|
|
read.GET("/deployments", handleGetRecentDeployments)
|
|
read.GET("/deployments/:id", handleGetDeployment)
|
|
|
|
read.GET("/databases", databaseHandler.GetDatabases)
|
|
read.GET("/databases/:id", databaseHandler.GetDatabase)
|
|
|
|
read.GET("/templates", handleGetTemplates)
|
|
read.GET("/templates/:id", handleGetTemplate)
|
|
|
|
read.GET("/cron-jobs", handleGetCronJobs)
|
|
read.GET("/cron-jobs/:id", handleGetCronJob)
|
|
read.GET("/cron-jobs/:id/executions", handleGetCronExecutions)
|
|
|
|
read.GET("/security/scans/:id", securityHandler.GetSecurityScan)
|
|
read.GET("/security/compliance/reports/:id", securityHandler.GetComplianceReport)
|
|
read.GET("/security/compliance/frameworks", securityHandler.GetComplianceFrameworks)
|
|
|
|
read.GET("/preview-environments/:id", handleGetPreviewEnvironment)
|
|
|
|
read.GET("/system/upgrade/status", handleGetUpgradeStatus)
|
|
read.GET("/system/host", handleGetHostMonitoring)
|
|
|
|
// HA + scaling read endpoints (status, policies, health, alerts)
|
|
haAPIManager.RegisterReadRoutes(read)
|
|
scalingHandler.RegisterReadRoutes(read)
|
|
}
|
|
|
|
// Authenticated routes — a valid session is required and handlers
|
|
// enforce ownership; platform admins additionally pass every check.
|
|
authed := v1.Group("/")
|
|
authed.Use(middleware.Auth(cfg.JWTSecret))
|
|
authed.Use(middleware.Idempotency())
|
|
{
|
|
authed.GET("/user/profile", handleGetProfile)
|
|
authed.PUT("/user/profile", handleUpdateProfile)
|
|
|
|
// Personal access tokens for CLI/MCP/agent auth.
|
|
authed.GET("/user/tokens", handleListUserTokens)
|
|
authed.POST("/user/tokens", handleCreateUserToken)
|
|
authed.DELETE("/user/tokens/:id", handleDeleteUserToken)
|
|
|
|
authed.GET("/notifications", handleListNotifications)
|
|
authed.POST("/notifications/:id/read", handleMarkNotificationRead)
|
|
authed.POST("/notifications/read-all", handleMarkAllNotificationsRead)
|
|
authed.GET("/notifications/channels", handleListNotificationChannels)
|
|
authed.POST("/notifications/channels", handleCreateNotificationChannel)
|
|
authed.POST("/notifications/channels/:id/test", handleTestNotificationChannel)
|
|
authed.DELETE("/notifications/channels/:id", handleDeleteNotificationChannel)
|
|
|
|
// WebSocket endpoint
|
|
authed.GET("/ws", handleWebSocket)
|
|
|
|
// Cross-cutting ops view — active/queued/failed work
|
|
authed.GET("/operations", handleGetOperations)
|
|
authed.GET("/nodes/options", agentHandler.ListNodeOptions)
|
|
authed.GET("/activity", handleGetActivity)
|
|
authed.GET("/projects/:id/activity", handleGetProjectActivity)
|
|
|
|
// Instance announcements shown to all signed-in users
|
|
authed.GET("/banners/active", handleListActiveBanners)
|
|
|
|
// Outbound webhooks — signed event delivery to user HTTP endpoints
|
|
authed.GET("/webhooks", handleListOutboundWebhooks)
|
|
authed.POST("/webhooks", handleCreateOutboundWebhook)
|
|
authed.PATCH("/webhooks/:id", handleUpdateOutboundWebhook)
|
|
authed.DELETE("/webhooks/:id", handleDeleteOutboundWebhook)
|
|
authed.GET("/webhooks/:id/deliveries", handleListOutboundWebhookDeliveries)
|
|
authed.POST("/webhooks/:id/test", handleTestOutboundWebhook)
|
|
|
|
// Owned-resource mutations — handlers scope to the caller and
|
|
// admit admins. New projects land unapproved until an admin
|
|
// publishes them.
|
|
authed.POST("/projects", handleCreateProject)
|
|
authed.POST("/projects/:id/services", handleCreateService)
|
|
authed.POST("/projects/:id/import-compose", handleImportProjectCompose)
|
|
authed.PUT("/projects/:id", handleUpdateProject)
|
|
authed.DELETE("/projects/:id", handleDeleteProject)
|
|
|
|
authed.PUT("/services/:id", handleUpdateService)
|
|
authed.DELETE("/services/:id", handleDeleteService)
|
|
authed.POST("/services/:id/domains", handleAddServiceDomain)
|
|
authed.DELETE("/services/:id/domains/:domain_id", handleDeleteServiceDomain)
|
|
authed.POST("/services/:id/domains/:domain_id/default", handleSetDefaultDomain)
|
|
authed.POST("/services/:id/start", handleServiceStart)
|
|
authed.POST("/services/:id/stop", handleServiceStop)
|
|
authed.POST("/services/:id/restart", handleServiceRestart)
|
|
authed.POST("/services/:id/sleep", handleServiceSleep)
|
|
authed.POST("/services/:id/wake", handleServiceWakeNow)
|
|
authed.POST("/services/:id/redeploy", handleServiceRedeploy)
|
|
authed.POST("/services/:id/clone", handleCloneService)
|
|
authed.POST("/services/:id/move", handleMoveService)
|
|
authed.POST("/services/:id/deployments", handleCreateDeployment)
|
|
authed.POST("/deployments/:id/rollback", handleRollbackDeployment)
|
|
authed.POST("/deployments/:id/cancel", handleCancelDeployment)
|
|
|
|
// Interactive terminal — WebSocket → docker exec pty.
|
|
authed.GET("/services/:id/terminal", handleServiceTerminal)
|
|
|
|
// Environment variables — secrets, owner/admin only.
|
|
authed.GET("/services/:id/variables", handleGetVariables)
|
|
authed.PUT("/services/:id/variables", handleUpdateVariables)
|
|
|
|
// Project-level shared variables — ${{shared.KEY}} refs.
|
|
authed.GET("/projects/:id/variables", handleGetProjectVariables)
|
|
authed.PUT("/projects/:id/variables", handleUpdateProjectVariables)
|
|
|
|
// Runtime logs — container stdout routinely echoes secrets,
|
|
// so these stay behind authentication like build logs.
|
|
authed.GET("/services/:id/logs", handleGetLogs)
|
|
authed.GET("/deployments/:id/logs", handleGetDeploymentLogs)
|
|
|
|
// One-off exec console (docker exec, 30s ceiling)
|
|
authed.POST("/services/:id/exec", handleExecInService)
|
|
|
|
// S3-compatible backup targets — owner-scoped, keys encrypted at rest.
|
|
authed.GET("/backup-targets", databaseHandler.ListBackupTargets)
|
|
authed.POST("/backup-targets", databaseHandler.CreateBackupTarget)
|
|
authed.PUT("/backup-targets/:id", databaseHandler.UpdateBackupTarget)
|
|
authed.DELETE("/backup-targets/:id", databaseHandler.DeleteBackupTarget)
|
|
authed.POST("/backup-targets/:id/test", databaseHandler.TestBackupTarget)
|
|
|
|
// Private-image registries — owner-scoped, credentials encrypted at rest.
|
|
authed.GET("/registries", handleListRegistries)
|
|
authed.POST("/registries", handleCreateRegistry)
|
|
authed.PUT("/registries/:id", handleUpdateRegistry)
|
|
authed.DELETE("/registries/:id", handleDeleteRegistry)
|
|
|
|
// Git integration — providers carry per-user credentials.
|
|
authed.GET("/git/github-app/install-url", handleGetGitHubAppInstallURL)
|
|
authed.POST("/git/github-app/connect", handleConnectGitHubApp)
|
|
authed.GET("/git/providers", handleGetGitProviders)
|
|
authed.POST("/git/providers", handleCreateGitProvider)
|
|
authed.DELETE("/git/providers/:providerId", handleDeleteGitProvider)
|
|
authed.GET("/git/providers/:providerId/repositories", handleGetGitRepositories)
|
|
authed.GET("/git/providers/:providerId/repositories/:owner/:repo/branches", handleGetGitRepositoryBranches)
|
|
authed.POST("/git/repositories/connect", handleConnectGitRepository)
|
|
authed.GET("/git/repositories", handleGetConnectedRepositories)
|
|
authed.POST("/git/webhooks", handleCreateWebhook)
|
|
|
|
// Builds have no per-user scoping and logs may carry secrets —
|
|
// keep every build endpoint behind authentication.
|
|
authed.POST("/builds", buildHandler.StartBuild)
|
|
authed.GET("/builds", buildHandler.ListBuilds)
|
|
authed.GET("/builds/:id", buildHandler.GetBuildStatus)
|
|
authed.GET("/builds/:id/logs", buildHandler.GetBuildLogs)
|
|
authed.GET("/builds/detect", buildHandler.DetectBuildType)
|
|
authed.POST("/builds/:id/cancel", buildHandler.CancelBuild)
|
|
authed.POST("/builds/plan", buildHandler.GetBuildPlan)
|
|
|
|
authed.POST("/databases", databaseHandler.CreateDatabase)
|
|
authed.POST("/databases/register-external", databaseHandler.RegisterExternalDatabase)
|
|
authed.POST("/databases/test-connection", databaseHandler.TestDatabaseConnection)
|
|
authed.POST("/databases/:id/test-connection", databaseHandler.TestDatabaseConnectionByID)
|
|
authed.PUT("/databases/:id", databaseHandler.UpdateDatabase)
|
|
authed.DELETE("/databases/:id", databaseHandler.DeleteDatabase)
|
|
authed.POST("/databases/:id/action", databaseHandler.PerformDatabaseAction)
|
|
authed.POST("/databases/:id/backup", databaseHandler.CreateBackup)
|
|
authed.POST("/databases/:id/restore", databaseHandler.RestoreBackup)
|
|
authed.POST("/databases/:id/backups/import", databaseHandler.ImportBackup)
|
|
authed.GET("/databases/:id/backups/:bid/download", databaseHandler.DownloadBackup)
|
|
|
|
authed.POST("/projects/:id/environments", handleCreateEnvironment)
|
|
authed.DELETE("/environments/:id", handleDeleteEnvironment)
|
|
authed.POST("/projects/:id/preview-environments", handleCreatePreviewEnvironment)
|
|
authed.PUT("/preview-environments/:id", handleUpdatePreviewEnvironment)
|
|
authed.DELETE("/preview-environments/:id", handleDeletePreviewEnvironment)
|
|
authed.POST("/preview-environments/:id/promote", handlePromotePreviewEnvironment)
|
|
|
|
authed.POST("/security/scans", securityHandler.StartSecurityScan)
|
|
authed.PUT("/vulnerabilities/:id", securityHandler.UpdateVulnerability)
|
|
authed.POST("/security/compliance/assess", securityHandler.StartComplianceAssessment)
|
|
authed.POST("/security/compliance/gdpr/init", securityHandler.InitializeGDPRFramework)
|
|
authed.GET("/projects/:id/security/audit-logs", securityHandler.GetAuditLogs)
|
|
|
|
// User templates — ownership enforced inside the handlers.
|
|
authed.POST("/templates", handleCreateTemplate)
|
|
authed.PUT("/templates/:id", handleUpdateTemplate)
|
|
authed.DELETE("/templates/:id", handleDeleteTemplate)
|
|
authed.POST("/templates/:id/deploy", handleCreateFromTemplate)
|
|
authed.POST("/templates/:id/plan", handlePlanTemplate)
|
|
authed.POST("/templates/import/compose", handleImportCompose)
|
|
authed.POST("/templates/import/git", handleImportGitCompose)
|
|
authed.POST("/templates/deploy", handleDeployInlineGraph)
|
|
|
|
authed.POST("/cron-jobs", handleCreateCronJob)
|
|
authed.PUT("/cron-jobs/:id", handleUpdateCronJob)
|
|
authed.DELETE("/cron-jobs/:id", handleDeleteCronJob)
|
|
authed.POST("/cron-jobs/:id/trigger", handleTriggerCronJob)
|
|
}
|
|
|
|
// Admin routes — platform-wide controls and cross-user management.
|
|
admin := v1.Group("/")
|
|
admin.Use(middleware.Auth(cfg.JWTSecret))
|
|
admin.Use(middleware.Idempotency())
|
|
admin.Use(middleware.RequireAdmin())
|
|
{
|
|
admin.GET("/admin/overview", handleAdminOverview)
|
|
admin.GET("/admin/users", handleAdminListUsers)
|
|
admin.PATCH("/admin/users/:id", handleAdminSetUserAdmin)
|
|
admin.POST("/admin/users/:id/impersonate", handleAdminImpersonateUser)
|
|
admin.PATCH("/admin/projects/:id", handleAdminSetProjectApproval)
|
|
|
|
admin.POST("/users", handleCreateUser)
|
|
|
|
admin.GET("/settings", handleGetSettings)
|
|
admin.PUT("/settings", handleUpdateSettings)
|
|
|
|
admin.POST("/system/upgrade/pull", handlePullUpgradeImage)
|
|
admin.POST("/preview-environments/cleanup-expired", handleCleanupExpiredPreviewEnvironments)
|
|
|
|
// HA + scaling + agent mutations — platform-level controls.
|
|
haAPIManager.RegisterAdminRoutes(admin)
|
|
scalingHandler.RegisterAdminRoutes(admin)
|
|
agentHandler.SetupAdminRoutes(admin)
|
|
|
|
admin.GET("/admin/volumes", handleAdminListVolumes)
|
|
admin.DELETE("/admin/volumes/:name", handleAdminDeleteVolume)
|
|
|
|
// Announcement banners
|
|
admin.GET("/admin/banners", handleAdminListBanners)
|
|
admin.POST("/admin/banners", handleAdminCreateBanner)
|
|
admin.PATCH("/admin/banners/:id", handleAdminUpdateBanner)
|
|
admin.DELETE("/admin/banners/:id", handleAdminDeleteBanner)
|
|
|
|
// GitHub App self-provisioning (manifest flow)
|
|
admin.GET("/admin/git/github-app", handleAdminGetGitHubApp)
|
|
admin.POST("/admin/git/github-app/manifest", handleAdminGitHubAppManifest)
|
|
admin.POST("/admin/git/github-app/convert", handleAdminGitHubAppConvert)
|
|
|
|
// Team invites — single-use registration links
|
|
admin.GET("/admin/invites", handleAdminListInvites)
|
|
admin.POST("/admin/invites", handleAdminCreateInvite)
|
|
admin.DELETE("/admin/invites/:id", handleAdminDeleteInvite)
|
|
admin.GET("/audit-logs", handleGetAuditLogs)
|
|
admin.GET("/audit-logs/:resource/:id", handleGetResourceAuditLogs)
|
|
|
|
// API Gateway routes (merged APwhy) - namespaced to avoid
|
|
// colliding with Containr's own /services paths.
|
|
gateway := admin.Group("/gateway")
|
|
{
|
|
gateway.GET("/services", handleAPwhyServicesList)
|
|
gateway.POST("/services", handleAPwhyServicesCreate)
|
|
gateway.PATCH("/services/:id", handleAPwhyServicesPatch)
|
|
gateway.GET("/services/:id", handleAPwhyServiceValidate)
|
|
|
|
gateway.GET("/keys", handleAPwhyKeysList)
|
|
gateway.POST("/keys", handleAPwhyKeysCreate)
|
|
gateway.PATCH("/keys/:id", handleAPwhyKeysPatch)
|
|
|
|
gateway.GET("/analytics/ops", handleAPwhyAnalyticsOps)
|
|
gateway.GET("/analytics/traffic", handleAPwhyAnalyticsTraffic)
|
|
}
|
|
}
|
|
}
|
|
}
|