Firebase Batch User Deleter

Automates the UI to batch delete users from the Firebase Authentication page, with an option to exclude specific emails.

Du musst eine Erweiterung wie Tampermonkey, Greasemonkey oder Violentmonkey installieren, um dieses Skript zu installieren.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

Sie müssten eine Skript Manager Erweiterung installieren damit sie dieses Skript installieren können

(Ich habe schon ein Skript Manager, Lass mich es installieren!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         Firebase Batch User Deleter
// @namespace    http://tampermonkey.net/
// @version      2.3
// @description  Automates the UI to batch delete users from the Firebase Authentication page, with an option to exclude specific emails.
// @author       MasuRii
// @match        https://console.firebase.google.com/u/*/project/*/authentication/users*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=firebase.google.com
// @grant        none
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // --- Constants ---
    const PROCESS_DELAY_MS = 1500; // Wait time after a deletion for the UI to update.

    // --- Helper Functions ---

    /**
     * Pauses execution for a specified duration.
     * @param {number} ms - The number of milliseconds to wait.
     */
    const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));

    /**
     * Waits for a specific element to appear in the DOM.
     * @param {string} selector - The CSS selector of the element to wait for.
     * @param {number} timeout - The maximum time to wait in milliseconds.
     * @returns {Promise<Element>} A promise that resolves with the element.
     */
    function waitForElement(selector, timeout = 5000) {
        return new Promise((resolve, reject) => {
            let observer = null;
            let timer = null;

            const findElement = () => {
                const element = document.querySelector(selector);
                if (element) {
                    if (observer) observer.disconnect();
                    if (timer) clearTimeout(timer);
                    resolve(element);
                    return true;
                }
                return false;
            };

            if (findElement()) return;

            observer = new MutationObserver(findElement);
            observer.observe(document.body, { childList: true, subtree: true });

            timer = setTimeout(() => {
                if (observer) observer.disconnect();
                reject(new Error(`Element not found after ${timeout}ms: ${selector}`));
            }, timeout);
        });
    }

    /**
     * Finds an element by its selector and specific text content.
     * @param {string} selector - The CSS selector to query.
     * @param {string} text - The exact text content to match.
     * @returns {Element|null} The found element or null.
     */
    function findElementByText(selector, text) {
        return Array.from(document.querySelectorAll(selector)).find(el => el.textContent.trim() === text);
    }

    /**
     * Creates and returns the UI elements for the script.
     * @returns {HTMLElement} The container element for the UI.
     */
    function createUI() {
        const container = document.createElement('div');
        container.id = 'batch-deleter-ui';
        container.style.padding = '16px';
        container.style.border = '1px solid #dadce0';
        container.style.borderRadius = '8px';
        container.style.marginBottom = '16px';
        container.style.backgroundColor = '#f8f9fa';

        const title = document.createElement('h3');
        title.textContent = 'Batch User Deleter';
        title.style.marginTop = '0';
        title.style.color = '#202124';
        container.appendChild(title);

        const description = document.createElement('p');
        description.innerHTML = 'Enter comma-separated emails to <b>EXCLUDE</b> from deletion. <br><b>Warning:</b> This process is slow. Do not interact with the page while it is running.';
        description.style.fontSize = '14px';
        description.style.color = '#5f6368';
        container.appendChild(description);

        const input = document.createElement('input');
        input.type = 'text';
        input.id = 'exclude-emails-input';
        input.placeholder = '[email protected], [email protected]';
        input.style.width = '100%';
        input.style.padding = '8px';
        input.style.marginBottom = '12px';
        input.style.boxSizing = 'border-box';
        container.appendChild(input);

        const button = document.createElement('button');
        button.textContent = 'Start Batch Deletion';
        button.id = 'batch-delete-btn-ui';
        button.style.backgroundColor = '#d93025';
        button.style.color = 'white';
        button.style.border = 'none';
        button.style.padding = '10px 16px';
        button.style.borderRadius = '4px';
        button.style.cursor = 'pointer';
        button.style.fontWeight = '500';
        container.appendChild(button);

        const statusDiv = document.createElement('div');
        statusDiv.id = 'batch-delete-status';
        statusDiv.style.marginTop = '12px';
        statusDiv.style.fontFamily = 'monospace';
        statusDiv.style.color = '#3c4043';
        statusDiv.textContent = 'Ready to start.';
        container.appendChild(statusDiv);

        button.addEventListener('click', () => handleBatchDelete(input, statusDiv));

        return container;
    }

    /**
     * Handles the entire batch deletion process.
     */
    async function handleBatchDelete(inputElement, statusElement) {
        const excludedEmails = inputElement.value.split(',')
            .map(email => email.trim().toLowerCase())
            .filter(email => email.length > 0);

        const userRows = Array.from(document.querySelectorAll('tr.mat-mdc-row.mdc-data-table__row'));
        if (userRows.length === 0) {
            statusElement.textContent = 'No user rows found on the page.';
            return;
        }

        const usersToDelete = userRows.filter(row => {
            const emailCell = row.querySelector('td.cdk-column-identifier .identifier-text');
            if (!emailCell) return true;
            const email = emailCell.textContent.trim().toLowerCase();
            return !excludedEmails.includes(email);
        });

        if (usersToDelete.length === 0) {
            statusElement.textContent = 'No users to delete after applying exclusion list.';
            return;
        }

        const confirmationMessage = `You are about to delete ${usersToDelete.length} user(s) by automating UI clicks.\n\n` +
            'This will be slow and you should not use this browser tab until it is finished.\n\n' +
            'Are you sure you want to proceed?';

        if (!window.confirm(confirmationMessage)) {
            statusElement.textContent = 'Operation cancelled by user.';
            return;
        }

        for (let i = 0; i < usersToDelete.length; i++) {
            const row = usersToDelete[i];
            const email = row.querySelector('td.cdk-column-identifier .identifier-text')?.textContent.trim() || `User #${i + 1}`;
            statusElement.textContent = `[${i + 1}/${usersToDelete.length}] Processing: ${email}`;

            try {
                const menuButton = row.querySelector('button[data-test-id="edit-account-button"]');
                if (!menuButton) throw new Error('Could not find menu button for user.');
                menuButton.click();

                await waitForElement('button[role="menuitem"]');
                const deleteButtonInMenu = findElementByText('button[role="menuitem"]', 'Delete account');
                if (!deleteButtonInMenu) throw new Error('"Delete account" option not found in menu.');
                deleteButtonInMenu.click();

                const confirmButton = await waitForElement('fire-dialog-actions .confirm-button');
                if (confirmButton.textContent.trim() !== 'Delete') throw new Error('Confirmation button text is not "Delete".');
                confirmButton.click();

                await delay(PROCESS_DELAY_MS);

            } catch (error) {
                console.error('Error during deletion process:', error);
                statusElement.textContent = `❌ Error on user ${email}: ${error.message}. Process halted.`;
                return;
            }
        }

        statusElement.textContent = `✅ Success! Processed ${usersToDelete.length} users. Please reload the page if necessary.`;
    }

    /**
     * Injects the UI onto the page when ready.
     */
    function initialize() {
        const observer = new MutationObserver((mutations, obs) => {
            const header = document.querySelector('.user-card-header');
            if (header && !document.getElementById('batch-deleter-ui')) {
                const ui = createUI();
                header.prepend(ui);
                obs.disconnect();
            }
        });

        observer.observe(document.body, {
            childList: true,
            subtree: true
        });
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', initialize);
    } else {
        initialize();
    }

})();