Files
Containr/app/backend/migrations_goose/20261010000000_service_domains.sql
T
Tomas DvorakandDevin e995c98d05 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>
2026-10-08 16:51:03 +02:00

31 lines
1.5 KiB
SQL

-- +goose Up
-- Multi-domain support. services.domain stays as the derived default for
-- compatibility; service_domains is the source of truth for the full
-- hostname set. Backfills existing services.domain rows as defaults.
CREATE TABLE IF NOT EXISTS service_domains (
id uuid DEFAULT gen_random_uuid() NOT NULL PRIMARY KEY,
service_id uuid NOT NULL REFERENCES services(id) ON DELETE CASCADE,
domain varchar(255) NOT NULL,
is_default boolean NOT NULL DEFAULT false,
cert_type varchar(32) NOT NULL DEFAULT 'letsencrypt',
cert_status varchar(32) NOT NULL DEFAULT 'pending',
last_checked_at timestamptz,
created_at timestamptz NOT NULL DEFAULT now(),
UNIQUE (service_id, domain)
);
CREATE INDEX IF NOT EXISTS idx_service_domains_service ON service_domains(service_id);
INSERT INTO service_domains (service_id, domain, is_default)
SELECT id, domain, true FROM services WHERE domain <> '' AND domain IS NOT NULL
ON CONFLICT (service_id, domain) DO NOTHING;
-- Access control: maintenance mode redirects to a platform page; basic auth
-- gates the route behind htpasswd-format user:hash pairs (Traefik middleware).
ALTER TABLE services ADD COLUMN IF NOT EXISTS maintenance_mode boolean NOT NULL DEFAULT false;
ALTER TABLE services ADD COLUMN IF NOT EXISTS basic_auth_users text NOT NULL DEFAULT '';
-- +goose Down
ALTER TABLE services DROP COLUMN IF EXISTS maintenance_mode;
ALTER TABLE services DROP COLUMN IF EXISTS basic_auth_users;
DROP TABLE IF EXISTS service_domains;