mirror of
https://github.com/Dvorinka/Containr.git
synced 2026-10-28 15:51:26 +00:00
* refactor(db): port operations, settings, audit, activity, and misc service metadata to sqlc Covers the cross-cutting surface: operations board reads, app settings, notification producers, activity/audit inserts, builds upsert/cancel, git clone credential resolution, invites, environments, registry auth, project/service variables, HA resolver, boot sweeps, and admin user ops. Dynamic filtered lists and Better Auth table probes stay raw by design. Authored By: TDvorak <[email protected]> * refactor(db): port apwhy gateway handlers and proxy data plane to sqlc Final tranche — every static call site now uses generated queries. Raw SQL remains only where the statement is assembled at runtime (filtered lists, sparse updates, admin count closure) or probes Better Auth's auth_users table, which lives outside the local schema. Authored By: TDvorak <[email protected]>
575 lines
23 KiB
Go
575 lines
23 KiB
Go
package api
|
|
|
|
import (
|
|
"context"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
"containr/internal/build"
|
|
"containr/internal/config"
|
|
"containr/internal/database"
|
|
"containr/internal/database/sqlcdb"
|
|
"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"
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
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) {
|
|
sid, err := uuid.Parse(serviceID)
|
|
if err != nil {
|
|
return "", 0, err
|
|
}
|
|
svc, err := sqlcdb.New(db.DB).GetServiceNameProjectPort(ctx, sid)
|
|
if err != nil {
|
|
return "", 0, err
|
|
}
|
|
if err := docker.ConnectSelfToNetwork(ctx, deployment.ProjectNetworkName(svc.ProjectID)); err != nil {
|
|
return "", 0, err
|
|
}
|
|
return svc.Name, int(svc.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.
|
|
bootQ := sqlcdb.New(db.DB)
|
|
_ = bootQ.FailInterruptedDeployments(context.Background())
|
|
|
|
// Boot sweep: encrypt any plaintext secret values left over from before
|
|
// encryption-at-rest landed. Transparent and idempotent.
|
|
if encRows, err := bootQ.ListSecretEnvVars(context.Background()); err == nil {
|
|
toEncrypt := 0
|
|
for _, r := range encRows {
|
|
if secrets.IsEncrypted(r.Value) {
|
|
continue
|
|
}
|
|
if err := bootQ.UpdateEnvVarValue(context.Background(), sqlcdb.UpdateEnvVarValueParams{
|
|
Value: secrets.Encrypt(r.Value), ID: r.ID,
|
|
}); err == nil {
|
|
toEncrypt++
|
|
}
|
|
}
|
|
if toEncrypt > 0 {
|
|
log.Printf("secrets: encrypted %d plaintext secret variable(s)", 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)
|
|
public.GET("/setup/status", handleSetupStatus)
|
|
|
|
// 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)
|
|
authed.POST("/setup/complete", handleSetupComplete)
|
|
|
|
// 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)
|
|
}
|
|
}
|
|
}
|
|
}
|