From 61881fd0c1a7066293f552be1a747143f70d03ff Mon Sep 17 00:00:00 2001 From: historia <[not public]> Date: Tue, 15 Sep 2026 20:44:31 -0400 Subject: fix: registration overwrite, recovery chip/payout edge cases, bulk-delete rollback --- internal/admin/api_rooms_bulk_delete.go | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) (limited to 'internal/admin/api_rooms_bulk_delete.go') diff --git a/internal/admin/api_rooms_bulk_delete.go b/internal/admin/api_rooms_bulk_delete.go index 18b10a5..70c2eb6 100644 --- a/internal/admin/api_rooms_bulk_delete.go +++ b/internal/admin/api_rooms_bulk_delete.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "io" + "log" "net/http" "os" @@ -38,6 +39,18 @@ func (s *AdminServer) handleBulkDelete(w http.ResponseWriter, r *http.Request) { return } + // Duplicate IDs in one request would delete the same file twice: the + // second os.Remove fails after the room is already gone, and the + // handler aborts without pushing an undo record. Reject up front. + seenIDs := make(map[int]bool, len(req.IDs)) + for _, id := range req.IDs { + if seenIDs[id] { + writeJSON(w, map[string]any{"error": fmt.Sprintf("duplicate room id %d in request", id)}) + return + } + seenIDs[id] = true + } + type roomSnapshot struct { id int path string @@ -63,6 +76,15 @@ func (s *AdminServer) handleBulkDelete(w http.ResponseWriter, r *http.Request) { for _, snap := range snapshots { s.detachRoomFromCourse(snap.id) if err := os.Remove(snap.path); err != nil { + // All-or-nothing: restore every room deleted so far so a failed + // bulk delete never strands rooms without an undo record. + for _, done := range snapshots { + if deletedSet[done.id] { + if werr := os.WriteFile(done.path, done.oldContent, 0644); werr != nil { + log.Printf("bulk-delete rollback of room %d: %v", done.id, werr) + } + } + } writeJSON(w, map[string]any{"error": fmt.Sprintf("delete room %d: %v", snap.id, err)}) return } -- cgit v1.2.3