diff --git a/db/migrations/20260328162803_template_uuid_pk.sql b/db/migrations/20260328162803_template_uuid_pk.sql index 8665241..0bb6566 100644 --- a/db/migrations/20260328162803_template_uuid_pk.sql +++ b/db/migrations/20260328162803_template_uuid_pk.sql @@ -12,6 +12,7 @@ ALTER TABLE templates ADD CONSTRAINT uq_templates_team_name UNIQUE (team_id, nam -- 3. Prevent team templates from using names that belong to global (platform) templates. -- A team template insert/update with a name matching any platform template is rejected. +-- +goose StatementBegin CREATE OR REPLACE FUNCTION check_global_template_name_collision() RETURNS TRIGGER AS $$ BEGIN @@ -28,13 +29,27 @@ BEGIN RETURN NEW; END; $$ LANGUAGE plpgsql; +-- +goose StatementEnd CREATE TRIGGER trg_check_global_template_name BEFORE INSERT OR UPDATE ON templates FOR EACH ROW EXECUTE FUNCTION check_global_template_name_collision(); --- 4. Add template UUID references to template_builds. +-- 4. Seed the built-in "minimal" template so it appears in all listings. +-- Both id and team_id are the all-zeros UUID (platform sentinel). +INSERT INTO templates (id, name, type, vcpus, memory_mb, size_bytes, team_id) +VALUES ( + '00000000-0000-0000-0000-000000000000', + 'minimal', + 'base', + 1, + 512, + 0, + '00000000-0000-0000-0000-000000000000' +) ON CONFLICT DO NOTHING; + +-- 5. Add template UUID references to template_builds. ALTER TABLE template_builds ADD COLUMN template_id UUID, ADD COLUMN team_id UUID; @@ -54,6 +69,9 @@ ALTER TABLE template_builds DROP COLUMN IF EXISTS team_id, DROP COLUMN IF EXISTS template_id; +-- Remove the seeded minimal template. +DELETE FROM templates WHERE id = '00000000-0000-0000-0000-000000000000'; + DROP TRIGGER IF EXISTS trg_check_global_template_name ON templates; DROP FUNCTION IF EXISTS check_global_template_name_collision(); diff --git a/internal/api/handlers_builds.go b/internal/api/handlers_builds.go index 58a7ed4..3b96400 100644 --- a/internal/api/handlers_builds.go +++ b/internal/api/handlers_builds.go @@ -12,6 +12,7 @@ import ( "git.omukk.dev/wrenn/sandbox/internal/db" "git.omukk.dev/wrenn/sandbox/internal/id" + "git.omukk.dev/wrenn/sandbox/internal/layout" "git.omukk.dev/wrenn/sandbox/internal/lifecycle" "git.omukk.dev/wrenn/sandbox/internal/service" "git.omukk.dev/wrenn/sandbox/internal/validate" @@ -221,6 +222,10 @@ func (h *buildHandler) DeleteTemplate(w http.ResponseWriter, r *http.Request) { writeError(w, http.StatusNotFound, "not_found", "template not found") return } + if layout.IsMinimal(tmpl.TeamID, tmpl.ID) { + writeError(w, http.StatusForbidden, "forbidden", "the minimal template cannot be deleted") + return + } // Broadcast delete to all online hosts. hosts, _ := h.db.ListActiveHosts(ctx) diff --git a/internal/api/handlers_snapshots.go b/internal/api/handlers_snapshots.go index 07bd030..f7d05f2 100644 --- a/internal/api/handlers_snapshots.go +++ b/internal/api/handlers_snapshots.go @@ -17,6 +17,7 @@ import ( "git.omukk.dev/wrenn/sandbox/internal/auth" "git.omukk.dev/wrenn/sandbox/internal/db" "git.omukk.dev/wrenn/sandbox/internal/id" + "git.omukk.dev/wrenn/sandbox/internal/layout" "git.omukk.dev/wrenn/sandbox/internal/lifecycle" "git.omukk.dev/wrenn/sandbox/internal/service" "git.omukk.dev/wrenn/sandbox/internal/validate" @@ -271,6 +272,10 @@ func (h *snapshotHandler) Delete(w http.ResponseWriter, r *http.Request) { writeError(w, http.StatusForbidden, "forbidden", "platform templates cannot be deleted here") return } + if layout.IsMinimal(tmpl.TeamID, tmpl.ID) { + writeError(w, http.StatusForbidden, "forbidden", "the minimal template cannot be deleted") + return + } if err := h.deleteSnapshotBroadcast(ctx, tmpl.TeamID, tmpl.ID); err != nil { writeError(w, http.StatusInternalServerError, "agent_error", "failed to delete snapshot files")