東京科学大学理工学部ポータル自動認証システム

改良版:自動で学籍番号、パスワード、マトリクス暗号を入力し、低遅延で自動送信します

// ==UserScript==
// @name         東京科学大学理工学部ポータル自動認証システム
// @namespace    http://tampermonkey.net/
// @version      0.1
// @description  改良版:自動で学籍番号、パスワード、マトリクス暗号を入力し、低遅延で自動送信します
// @author       https://github.com/catyyy
// @match        https://portal.nap.gsic.titech.ac.jp/GetAccess/Login*
// @grant        GM_registerMenuCommand
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_addStyle
// @require      https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js
// @run-at       document-end
// @license MIT
// ==/UserScript==

(function($) {
    'use strict';

    // スタイル定義
    GM_addStyle(`
        .auth-helper {
            position: fixed;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            background: #ffffff;
            padding: 2rem;
            border-radius: 12px;
            box-shadow: 0 8px 32px rgba(0,0,0,0.2);
            z-index: 99999;
            min-width: 600px;
            font-family: 'Segoe UI', sans-serif;
        }
        .matrix-container {
            display: grid;
            grid-template-columns: auto 1fr;
            gap: 10px;
            margin: 1rem 0;
        }
        .row-labels {
            display: grid;
            gap: 5px;
            grid-template-rows: repeat(7, 45px);
        }
        .row-label {
            display: flex;
            align-items: center;
            justify-content: center;
            background: #f5f5f5;
            border-radius: 4px;
        }
        .matrix-grid {
            display: grid;
            grid-template-columns: repeat(10, 45px);
            grid-auto-rows: 45px;
            gap: 5px;
        }
        .matrix-input {
            width: 100%;
            height: 100%;
            border: 2px solid #ddd;
            text-align: center;
            font-size: 16px;
            text-transform: uppercase;
            transition: all 0.3s;
            border-radius: 4px;
        }
        .matrix-input:focus {
            border-color: #2196F3;
            background: #e3f2fd;
            outline: none;
            box-shadow: 0 2px 6px rgba(33,150,243,0.3);
        }
        .col-labels {
            display: grid;
            grid-template-columns: repeat(10, 45px);
            gap: 5px;
            margin-bottom: 5px;
        }
        .col-label {
            text-align: center;
            font-weight: bold;
            color: #666;
        }
        .auth-toast {
            position: fixed;
            top: 20px;
            right: 20px;
            padding: 12px 24px;
            border-radius: 8px;
            color: white;
            background: #00C851;
            box-shadow: 0 4px 12px rgba(0,0,0,0.15);
            z-index: 99999;
            animation: slideIn 0.3s;
        }
        .auth-toast.error {
            background: #ff4444;
        }
        @keyframes slideIn {
            from { transform: translateX(100%); }
            to { transform: translateX(0); }
        }
        .save-footer {
            margin-top: 1rem;
            text-align: right;
        }
    `);

    // ストレージシステム
    const Storage = {
        getCredentials: () => {
            try {
                return JSON.parse(GM_getValue('auth_creds', '{}'));
            } catch (e) {
                showToast('設定の読み込みに失敗しました', 'error');
                return {};
            }
        },
        saveCredentials: (data) => {
            try {
                GM_setValue('auth_creds', JSON.stringify(data));
                return true;
            } catch (e) {
                showToast('保存に失敗しました: ストレージ容量不足', 'error');
                return false;
            }
        },
        getMatrixMap: () => {
            try {
                const map = JSON.parse(GM_getValue('matrix_map', '{}'));
                return Object.keys(map).length > 0 ? map : null;
            } catch (e) {
                return null;
            }
        },
        saveMatrixMap: (map) => {
            try {
                GM_setValue('matrix_map', JSON.stringify(map));
                return true;
            } catch (e) {
                showToast('マトリクス設定の保存に失敗しました', 'error');
                return false;
            }
        },
        isConfigured: () => {
            const creds = Storage.getCredentials();
            return !!creds.username && !!Storage.getMatrixMap();
        }
    };

    // UIコンポーネント:トーストメッセージ表示
    function showToast(message, type = 'info') {
        const toast = $(`<div class="auth-toast ${type === 'error' ? 'error' : ''}">${message}</div>`);
        $('body').append(toast);
        setTimeout(() => toast.fadeOut(), 2000);
    }

    // ログイン情報設定ウィザード
    function showCredentialWizard() {
        const creds = Storage.getCredentials() || {};
        const html = `
            <div class="auth-helper">
                <h3>🔑 ログイン情報設定</h3>
                <input type="text"
                       id="cred-username"
                       placeholder="学籍番号"
                       value="${creds.username || ''}"
                       style="margin: 8px 0; width: 100%; padding: 8px;">
                <input type="password"
                       id="cred-password"
                       placeholder="パスワード"
                       value="${creds.password || ''}"
                       style="margin: 8px 0; width: 100%; padding: 8px;">
                <div class="save-footer">
                    <button id="save-credentials"
                            style="background: #2196F3; color: white; padding: 8px 16px;">
                        保存して閉じる
                    </button>
                </div>
            </div>
        `;
        const $wrapper = $(html).appendTo('body');

        $('#save-credentials').on('click', function() {
            const data = {
                username: $('#cred-username').val().trim(),
                password: $('#cred-password').val().trim()
            };

            if (!data.username || !data.password) {
                showToast('すべての項目を入力してください', 'error');
                return;
            }

            if (Storage.saveCredentials(data)) {
                showToast('正常に保存されました');
                $wrapper.remove();
                if (!Storage.getMatrixMap()) {
                    showMatrixEditor(true);
                }
            }
        });
    }

    // マトリクスエディタ
    function showMatrixEditor(initialSetup = false) {
        let currentMap = Storage.getMatrixMap() || {};
        const colLabels = ['A','B','C','D','E','F','G','H','I','J'];

        let gridHTML = '';
        gridHTML += `<div class="col-labels">`;
        colLabels.forEach(label => gridHTML += `<div class="col-label">${label}</div>`);
        gridHTML += `</div>`;

        gridHTML += `<div class="matrix-container">`;
        gridHTML += `<div class="row-labels">`;
        for (let row = 1; row <= 7; row++) {
            gridHTML += `<div class="row-label">${row}</div>`;
        }
        gridHTML += `</div>`;

        gridHTML += `<div class="matrix-grid">`;
        for (let row = 1; row <= 7; row++) {
            colLabels.forEach(col => {
                const key = `${col},${row}`;
                gridHTML += `
                    <input type="text"
                           class="matrix-input"
                           data-key="${key}"
                           value="${currentMap[key] || ''}"
                           maxlength="1">
                `;
            });
        }
        gridHTML += `</div></div>`;

        const html = `
            <div class="auth-helper">
                <h3>🔢 マトリクス暗号設定</h3>
                ${gridHTML}
                <div class="save-footer">
                    <button id="save-matrix"
                            style="background: #4CAF50; color: white; padding: 8px 16px;">
                        保存して閉じる
                    </button>
                </div>
                <div style="margin-top:1rem; color:#666; font-size:0.9em;">
                    ※ 入力後自動で次のセルに移動(方向キーも使用可)
                </div>
            </div>
        `;
        const $wrapper = $(html).appendTo('body');
        const $inputs = $wrapper.find('.matrix-input');

        // 入力処理:大文字変換と自動移動
        $inputs.on('input', function() {
            const $input = $(this);
            const value = $input.val().toUpperCase();
            const key = $input.data('key');
            if (!/^[A-Z]$/.test(value)) {
                $input.val('');
                return;
            }
            currentMap[key] = value;
            moveToNextCell($input);
        });

        // キーボードナビゲーション
        $inputs.on('keydown', function(e) {
            const $input = $(this);
            const index = $inputs.index($input);
            const key = e.key.toLowerCase();
            const navigation = {
                arrowright: () => moveFocus(index + 1),
                arrowleft: () => moveFocus(index - 1),
                arrowdown: () => moveFocus(index + 10),
                arrowup: () => moveFocus(index - 10),
                enter: () => {
                    if (initialSetup) return;
                    moveToNextCell($input);
                }
            };
            if (navigation[key]) {
                e.preventDefault();
                navigation[key]();
            }
        });

        // 保存処理
        $('#save-matrix').on('click', function() {
            if (Object.keys(currentMap).length < 10) {
                showToast('少なくとも10セル以上入力してください', 'error');
                return;
            }
            if (Storage.saveMatrixMap(currentMap)) {
                showToast('マトリクス設定を保存しました');
                $wrapper.remove();
                if (initialSetup) {
                    showToast('初期設定が完了しました');
                }
            }
        });

        $inputs.first().focus();
    }

    // ナビゲーション補助
    function moveToNextCell($current) {
        const index = $('.matrix-input').index($current);
        moveFocus(index + 1);
    }
    function moveFocus(newIndex) {
        const $inputs = $('.matrix-input');
        newIndex = Math.max(0, Math.min(newIndex, $inputs.length - 1));
        $inputs.eq(newIndex).focus().select();
    }

    // 自動ログイン:学籍番号とパスワードを入力し、OK ボタンをクリック
    function autoLogin() {
        if (!Storage.isConfigured()) {
            showToast('初期設定が必要です', 'error');
            showCredentialWizard();
            return;
        }
        const creds = Storage.getCredentials();
        const $username = $('input[name="usr_name"]');
        const $password = $('input[name="usr_password"]');
        if ($username.length && $password.length) {
            $username.val(creds.username);
            $password.val(creds.password);
            $('input[name="OK"]').trigger('click');
        }
    }

    // 自動マトリクス入力:id="authentication" 内の各行を走査し、[B,4] 等のラベルに対応する値を入力
    function autoMatrixFill() {
        const matrixMap = Storage.getMatrixMap();
        if (!matrixMap) {
            showToast('マトリクス設定が未保存です', 'error');
            return;
        }
        $('#authentication tr').each(function() {
            const $tr = $(this);
            const labelText = $tr.find('th').first().text().trim();
            const m = labelText.match(/\[\s*([A-J])\s*,\s*(\d)\s*\]/);
            if (m) {
                const key = `${m[1]},${m[2]}`;
                const value = matrixMap[key] || '';
                $tr.find('input[type="password"]').val(value);
            }
        });
        // 入力完了後、500ms 遅延して OK ボタンを自動クリック
        setTimeout(() => {
            $('input[name="OK"]').trigger('click');
        }, 500);
    }

    // 右クリックメニューの登録
    GM_registerMenuCommand("⚙️ ログイン情報設定", () => showCredentialWizard());
    GM_registerMenuCommand("🔣 マトリクス設定", () => showMatrixEditor());

    // ページ読み込み完了時の処理
    $(document).ready(function() {
        if (location.href.includes('Login')) {
            if (Storage.isConfigured()) {
                autoLogin();
            } else {
                showCredentialWizard();
            }
        }
    });

    // すべての onLoad 処理完了後、150ms 遅延して自動マトリクス入力と自動送信を実行
    window.addEventListener('load', () => {
        setTimeout(autoMatrixFill, 150);
    });

})(jQuery);