package handlers import ( "encoding/json" "log" "net/http" "regexp" ) var validUsername = regexp.MustCompile(`^[a-z0-9][a-z0-9_-]*$`) // handleDeleteUser fully deletes an account: LDAP user + customer stack and volumes. // Requires ADMIN_SECRET env set and X-Admin-Secret header. POST /admin/delete-user?user=username 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 } username := r.URL.Query().Get("user") if username == "" { username = r.FormValue("user") } if username == "" { http.Error(w, "user required", http.StatusBadRequest) return } if !validUsername.MatchString(username) { http.Error(w, "invalid username", http.StatusBadRequest) return } if err := a.ldap.DeleteUser(username); err != nil { log.Printf("admin delete-user %s: ldap: %v", username, err) w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusInternalServerError) json.NewEncoder(w).Encode(map[string]string{"error": err.Error()}) return } stackName := "customer-" + username if err := a.swarm.RemoveStackAndVolumes(stackName); err != nil { log.Printf("admin delete-user %s: stack/volumes: %v", username, err) // LDAP user 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": username}) }