feat(services): multi-domain, DNS check, maintenance + basic-auth gates (C2)

service_domains table holds the full hostname set; services.domain stays
the derived default and is synced on every domain write. Runtime spec
carries all domains into a multi-Host Traefik rule plus access gates.

- GET|POST /services/:id/domains, DELETE + POST /:domain_id/default
- GET /services/:id/domains/check — DNS preflight vs public_ip/base_url
  (ok | wrong-target | pending), stamps last_checked_at
- maintenance_mode → Traefik redirectregex to /api/v1/maintenance
  (base_url/BASE_URL setting; falls back to noop@internal offline)
- basic_auth [{username,password}] → bcrypt htpasswd on the label;
  responses return usernames only
- First domain auto-defaults; domain="" via PUT clears all domains
- CLI: services domains list|add|remove|default|check, update
  --maintenance/--basic-auth; MCP: 5 domain tools + update args
- Settings UI: Domains card (CRUD + DNS check badges) + Access card
- Fixes latent C1 bug: PUT without volumes 500'd — nil []byte sent as
  bytea, not NULL; COALESCE needs untyped nil + ::jsonb cast

Verified live: domain CRUD + default sync, pending/wrong-target checks,
bcrypt htpasswd and both middlewares on the deployed container's labels.

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

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Tomas DvorakandDevin committed 2026-10-08 16:51:03 +02:00
1 parent 2f8f595d6e
commit e995c98d05
19 files changed
+1565 -42

No files matched your search

+6
View File
@@ -248,6 +248,7 @@ func SetupRoutes(router *gin.Engine, db *database.DB, redis *database.Redis, cfg
public.GET("/auth/bootstrap", handleAuthBootstrap)
public.POST("/auth/login", handleLogin)
public.POST("/auth/register", handleRegister)
public.GET("/maintenance", handleMaintenancePage)
}
// Read routes — public browsing. OptionalAuth resolves a session when
@@ -268,6 +269,8 @@ func SetupRoutes(router *gin.Engine, db *database.DB, redis *database.Redis, cfg
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("/deployments", handleGetRecentDeployments)
read.GET("/deployments/:id", handleGetDeployment)
@@ -326,6 +329,9 @@ func SetupRoutes(router *gin.Engine, db *database.DB, redis *database.Redis, cfg
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)