package handlers import ( "encoding/json" "log" "net/http" "regexp" ) var validUsername = regexp.MustCompile(`^[a-z0-9][a-z0-9_-]*$`) // handleDeleteUser fully deletes an account plus its customer stack and volumes. // Requires ADMIN_SECRET env set and X-Admin-Secret header. POST /admin/delete-user?user=instance-slug func (a *App) handleDeleteUser(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } if a.cfg.AdminSecret == "" { http.NotFound(w, r) return } secret := r.Header.Get("X-Admin-Secret") if secret != a.cfg.AdminSecret { http.Error(w, "forbidden", http.StatusForbidden) return } slug := r.URL.Query().Get("user") if slug == "" { slug = r.FormValue("user") } if slug == "" { http.Error(w, "user required", http.StatusBadRequest) return } if !validUsername.MatchString(slug) { http.Error(w, "invalid username", http.StatusBadRequest) return } inst, err := a.accounts.DeleteAccountByInstanceSlug(r.Context(), slug) if err != nil { log.Printf("admin delete-user %s: account: %v", slug, err) w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusInternalServerError) json.NewEncoder(w).Encode(map[string]string{"error": err.Error()}) return } if err := a.swarm.RemoveStackAndVolumes(inst.StackName); err != nil { log.Printf("admin delete-user %s: stack/volumes: %v", slug, err) // Account already deleted; report but don't fail. w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(map[string]string{ "status": "user deleted", "warning": "stack/volumes: " + err.Error(), }) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]string{"status": "deleted", "user": slug}) }