Stackblitz Account Selector (RU)

Интерфейс для сайта stackblitz предоставляющую возможность загружать свою базу аккаунтов (локально) и использовать ее для упрощения логина в разные аккаунты

// ==UserScript==
// @name         Stackblitz Account Selector (RU)
// @namespace    http://tampermonkey.net/
// @version      3.5
// @description  Интерфейс для сайта stackblitz предоставляющую возможность загружать свою базу аккаунтов (локально) и использовать ее для упрощения логина в разные аккаунты
// @author       t.me/dud3lk
// @match        https://stackblitz.com/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_deleteValue
// @license      GNU GPLv3
// ==/UserScript==

(function () {
    'use strict';


    const BACKGROUND_COLOR = '#1D1F24';
    const BUTTON_LOGIN_COLOR = '#28a745';
    const BUTTON_DELETE_COLOR = '#dc3545';
    const BUTTON_UPLOAD_COLOR = '#007bff';


    let logoutObserver = null;
    let lastUsedAccount = null;


    if (window.location.href.includes('sign_in')) {
        handleLoginPage();
    } else {
        handleMainPage();
    }


    function handleLoginPage() {
        waitForLoginFields();
        startLogoutDetection();
    }

    function handleMainPage() {
        waitForSignInButton();
        startLogoutDetection();
    }



    function startLogoutDetection() {
        if (logoutObserver) return;

        logoutObserver = new MutationObserver((mutations) => {
            mutations.forEach((mutation) => {
                if (mutation.addedNodes.length) {
                    const signInLink = document.querySelector('a[href="/sign_in"]');
                    if (signInLink) {
                        handleLogoutEvent();
                    }
                }
            });
        });

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

    function handleLogoutEvent() {
        const email = GM_getValue('lastUsedAccount', null);
        if (email) {
            logLogout(email);
            GM_deleteValue('lastUsedAccount');


            const container = document.getElementById('account-selector-container');
            if (container) {
                container.remove();
                showAccountSelector();
            }
        }
    }

    function logLogout(email) {
        const logs = GM_getValue('logoutLogs', {});
        logs[email] = new Date().toISOString();
        GM_setValue('logoutLogs', logs);
        console.log(`[LOGOUT] ${email} at ${logs[email]}`);
    }


    function logLogin(email) {
        const logs = GM_getValue('loginLogs', {});
        logs[email] = new Date().toISOString();
        GM_setValue('loginLogs', logs);

        GM_setValue('lastUsedAccount', email);
        console.log(`[LOGIN] ${email} at ${logs[email]}`);
    }


    function showAccountSelector() {
        const accounts = GM_getValue('accounts', []);
        const logoutLogs = GM_getValue('logoutLogs', {});
        const loginLogs = GM_getValue('loginLogs', {});

        if (accounts.length === 0) {
            alert('Нет доступных аккаунтов. Загрузите файл.');
            return;
        }

        const container = document.createElement('div');
        container.id = 'account-selector-container';
        container.style.position = 'fixed';
        container.style.bottom = '10px';
        container.style.right = '10px';
        container.style.backgroundColor = BACKGROUND_COLOR;
        container.style.padding = '20px';
        container.style.border = '1px solid #333';
        container.style.zIndex = 9999;
        container.style.boxShadow = '0 4px 6px rgba(0, 0, 0, 0.1)';
        container.style.borderRadius = '8px';
        container.style.width = '450px';
        container.style.maxHeight = '600px';
        container.style.overflowY = 'auto';
        container.style.color = '#fff';

        const title = document.createElement('h3');
        title.textContent = 'Выберите аккаунт:';
        title.style.marginBottom = '15px';
        title.style.color = '#aaa';
        title.style.fontSize = '16px';

        const list = document.createElement('ul');
        list.style.listStyleType = 'none';
        list.style.padding = '0';
        list.style.margin = '0';

        accounts.forEach(account => {
            const [email] = account.split(':');
            const lastLogout = logoutLogs[email];
            const lastLogin = loginLogs[email];

            const listItem = document.createElement('li');
            listItem.style.display = 'flex';
            listItem.style.justifyContent = 'space-between';
            listItem.style.alignItems = 'center';
            listItem.style.padding = '10px';
            listItem.style.borderBottom = '1px solid #333';
            listItem.style.cursor = 'pointer';
            listItem.style.transition = 'background-color 0.3s';

            const emailSpan = document.createElement('span');
            emailSpan.style.display = 'flex';
            emailSpan.style.flexDirection = 'column';
            emailSpan.style.gap = '5px';

            const emailText = document.createElement('span');
            emailText.textContent = email;
            emailText.style.color = '#ccc';
            emailText.style.fontSize = '14px';

            const lastLoginSpan = document.createElement('span');
            lastLoginSpan.style.color = '#888';
            lastLoginSpan.style.fontSize = '12px';
            lastLoginSpan.textContent = lastLogin
                ? `Вход: ${new Date(lastLogin).toLocaleString()}`
                : '';

            const lastLogoutSpan = document.createElement('span');
            lastLogoutSpan.style.color = '#888';
            lastLogoutSpan.style.fontSize = '12px';

            if (lastLogout) {
                const logoutDate = new Date(lastLogout);
                const now = new Date();
                const diffHours = Math.floor((now - logoutDate) / 36e5);

                lastLogoutSpan.textContent = `Выход: ${logoutDate.toLocaleString()}`;

                if (diffHours < 24) {
                    lastLogoutSpan.textContent += ` (Заблокирован на ${24 - diffHours}ч)`;
                }
            } else {
                lastLogoutSpan.textContent = 'Выходов не было';
            }

            emailSpan.appendChild(emailText);
            emailSpan.appendChild(lastLoginSpan);
            emailSpan.appendChild(lastLogoutSpan);

            const loginButton = createButton('Войти', BUTTON_LOGIN_COLOR);
            loginButton.style.padding = '5px 10px';
            loginButton.style.fontSize = '12px';

            if (lastLogout) {
                const logoutDate = new Date(lastLogout);
                const now = new Date();
                const diffHours = Math.floor((now - logoutDate) / 36e5);

                if (diffHours < 24) {
                    loginButton.style.backgroundColor = BUTTON_DELETE_COLOR;
                    loginButton.title = `Доступно через: ${24 - diffHours} часов`;
                }
            }

            loginButton.addEventListener('click', () => {
                GM_setValue('selectedAccount', account);
                redirectToSignIn();
                container.remove();
            });

            listItem.appendChild(emailSpan);
            listItem.appendChild(loginButton);
            list.appendChild(listItem);
        });

        const deleteButton = createButton('Удалить аккаунты', BUTTON_DELETE_COLOR);
        deleteButton.style.marginTop = '15px';
        deleteButton.addEventListener('click', () => {
            GM_deleteValue('accounts');
            GM_deleteValue('logoutLogs');
            GM_deleteValue('loginLogs');
            alert('Все данные удалены.');
            container.remove();
            showUploadBasePrompt();
        });

        container.appendChild(title);
        container.appendChild(list);
        container.appendChild(deleteButton);
        document.body.appendChild(container);
    }



    function createButton(text, color) {
        const button = document.createElement('button');
        button.textContent = text;
        button.style.display = 'inline-block';
        button.style.padding = '10px 20px';
        button.style.backgroundColor = color;
        button.style.color = '#fff';
        button.style.border = 'none';
        button.style.borderRadius = '4px';
        button.style.cursor = 'pointer';
        button.style.fontSize = '14px';
        button.style.marginTop = '10px';
        return button;
    }

    function redirectToSignIn() {
        waitForElement('a[href="/sign_in"]', (signInButton) => {
            signInButton.click();
            const checkForSignInPage = setInterval(() => {
                if (window.location.href.includes('sign_in')) {
                    clearInterval(checkForSignInPage);
                    waitForLoginFields();
                }
            }, 200);
            setTimeout(() => {
                clearInterval(checkForSignInPage);
                alert('Переход на страницу входа не выполнен. Попробуйте войти вручную.');
            }, 10000);
        });
    }

    function waitForLoginFields() {

        waitForElement('input[name="login"], input[name="password"], button[type="submit"]', () => {
            const selectedAccount = GM_getValue('selectedAccount', null);
            if (selectedAccount) {
                const [email, password] = selectedAccount.split(':');
                performLogin(email, password);
            } else {
                alert('Не выбран аккаунт. Вернитесь на главную страницу и выберите аккаунт.');
            }
        });
    }

    function performLogin(email, password) {
        const emailInput = document.querySelector('input[name="login"]');
        const passwordInput = document.querySelector('input[name="password"]');

        const submitButton = document.querySelector('button[type="submit"]');

        if (emailInput && passwordInput && submitButton) {
            emailInput.value = email;
            passwordInput.value = password;
            setTimeout(() => {
                submitButton.click();
                logLogin(email);
                GM_deleteValue('selectedAccount');
            }, 500);
        } else {
            alert('Не удалось найти поля для входа. Пожалуйста, войдите вручную.');
        }
    }

    function waitForSignInButton() {
        waitForElement('a._link_kkm2m_1[href="/sign_in"]', () => {
            loadAccounts();
            showAccountSelector();
        });

    }

    function waitForElement(selector, callback) {
        const observer = new MutationObserver((mutations) => {
            mutations.forEach((mutation) => {
                const element = document.querySelector(selector);
                if (element) {
                    observer.disconnect();
                    callback(element);
                }
            });
        });
        observer.observe(document.body, { childList: true, subtree: true });
    }

    function loadAccounts() {
        const accounts = GM_getValue('accounts', null);
        if (!accounts) {
            showUploadBasePrompt();
        }
    }

    function showUploadBasePrompt() {
        const container = document.createElement('div');
        container.style.position = 'fixed';
        container.style.bottom = '10px';
        container.style.right = '10px';
        container.style.backgroundColor = BACKGROUND_COLOR;
        container.style.padding = '10px';
        container.style.border = '1px solid #333';
        container.style.zIndex = 9999;
        container.style.boxShadow = '0 4px 6px rgba(0, 0, 0, 0.1)';
        container.style.borderRadius = '5px';
        container.style.color = '#fff';

        const label = document.createElement('label');
        label.textContent = 'Загрузите базу аккаунтов:';
        label.style.display = 'block';
        label.style.marginBottom = '10px';
        label.style.color = '#aaa';

        const uploadButton = createButton('Загрузить базу', BUTTON_UPLOAD_COLOR);
        uploadButton.addEventListener('click', () => {
            createFileInput();
            container.remove();
        });

        container.appendChild(label);
        container.appendChild(uploadButton);
        document.body.appendChild(container);
    }

    function createFileInput() {
        const fileInputContainer = document.createElement('div');
        fileInputContainer.style.position = 'fixed';
        fileInputContainer.style.top = '10px';
        fileInputContainer.style.left = '10px';
        fileInputContainer.style.backgroundColor = BACKGROUND_COLOR;
        fileInputContainer.style.padding = '10px';
        fileInputContainer.style.border = '1px solid #333';
        fileInputContainer.style.zIndex = 9999;
        fileInputContainer.style.boxShadow = '0 4px 6px rgba(0, 0, 0, 0.1)';
        fileInputContainer.style.borderRadius = '5px';
        fileInputContainer.style.color = '#fff';

        const label = document.createElement('label');
        label.textContent = 'Выберите файл:';
        label.style.display = 'block';
        label.style.marginBottom = '10px';
        label.style.color = '#aaa';

        const fileInput = document.createElement('input');
        fileInput.type = 'file';
        fileInput.accept = '.txt';
        fileInput.style.display = 'block';

        fileInput.addEventListener('change', (event) => {
            const file = event.target.files[0];
            if (file) {
                const reader = new FileReader();
                reader.onload = (e) => {
                    const content = e.target.result;
                    const accountList = content.split('\n').map(line => line.trim()).filter(line => line !== '');
                    GM_setValue('accounts', accountList);
                    alert('Аккаунты успешно загружены!');
                    fileInputContainer.remove();
                    showAccountSelector();
                };
                reader.readAsText(file);
            }
        });

        fileInputContainer.appendChild(label);
        fileInputContainer.appendChild(fileInput);
        document.body.appendChild(fileInputContainer);
        fileInput.click();
    }
})();