| {% extends "base.html" %}
|
|
|
| {% block title %}Token管理{% endblock %}
|
|
|
| {% block extra_css %}
|
| <style>
|
| .token-list {
|
| width: 100%;
|
| border-collapse: collapse;
|
| }
|
|
|
| .token-list th,
|
| .token-list td {
|
| border: 1px solid #e2e8f0;
|
| padding: 8px;
|
| text-align: left;
|
| }
|
|
|
| .token-list th {
|
| background-color: #f8fafc;
|
| }
|
|
|
| .token-list tr:nth-child(even) {
|
| background-color: #f1f5f9;
|
| }
|
|
|
| .truncate {
|
| max-width: 200px;
|
| white-space: nowrap;
|
| overflow: hidden;
|
| text-overflow: ellipsis;
|
| }
|
|
|
| h2 {
|
| position: relative;
|
| }
|
|
|
| #addTokenBtn {
|
| position: absolute;
|
| font-size: 1rem;
|
| right: 0;
|
| top: 50%;
|
| transform: translateY(-50%);
|
| }
|
|
|
| button:disabled {
|
| color: #ccc;
|
| background-color: #eee;
|
| border: none;
|
| cursor: not-allowed;
|
| }
|
|
|
| button:disabled:hover {
|
| color: #ccc;
|
| background-color: #eee;
|
| border: none;
|
| cursor: not-allowed;
|
| }
|
|
|
| @media (max-width: 770px) {
|
| body {
|
| font-size: 2.3vw;
|
| }
|
|
|
| h2 {
|
| font-size: 3.5vw !important;
|
| }
|
|
|
| #addTokenBtn {
|
| font-size: 2vw;
|
| line-height: 2vw;
|
| }
|
|
|
| .token-list {
|
| flex: 1;
|
| }
|
|
|
| .token-list thead {
|
| display: flex;
|
| flex-wrap: wrap;
|
| }
|
|
|
| .token-list #tokenTableBody {
|
| display: flex;
|
| flex-wrap: wrap;
|
| }
|
|
|
| .token-list tr {
|
| display: flex;
|
| }
|
|
|
| .token-list tr,
|
| .token-list th,
|
| .token-list td {
|
| flex: 1;
|
| }
|
| }
|
| </style>
|
| {% endblock %}
|
|
|
| {% block content %}
|
| <div class="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4">
|
| <h2 class="text-2xl font-bold text-gray-800 mb-4">账号列表
|
| <button id="addTokenBtn"
|
| class="bg-green-500 hover:bg-green-600 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline transition duration-300 ease-in-out mr-2">
|
| 添加账号
|
| </button>
|
| </h2>
|
| <table class="token-list">
|
| <thead>
|
| <tr>
|
| <th>Email</th>
|
| <th>Re Token</th>
|
| <th>状态</th>
|
| <th>操作</th>
|
| </tr>
|
| </thead>
|
| <tbody id="tokenTableBody">
|
|
|
| </tbody>
|
| </table>
|
| <div class="flex items-center justify-between mt-4">
|
| <div>
|
| <button id="refreshBtn"
|
| class="bg-yellow-500 hover:bg-yellow-600 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline transition duration-300 ease-in-out mr-2">
|
| 立即刷新 Tokens
|
| </button>
|
| </div>
|
| <p id="statusMessage" class="italic"></p>
|
| </div>
|
| </div>
|
|
|
| <div class="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4">
|
| <h2 class="text-2xl font-bold text-gray-800 mb-4">Access Token刷新设置</h2>
|
| <div class="flex items-center mb-4">
|
| <input type="checkbox" id="autoRefreshToggle" class="mr-2">
|
| <label for="autoRefreshToggle" class="mr-4">启用自动刷新</label>
|
| <input type="number" id="refreshInterval" min="1" value="1" class="border rounded px-2 py-1 w-20 mr-2">
|
| <span>天</span>
|
| </div>
|
| <p id="nextRefreshTime" class="text-gray-600"></p>
|
| </div>
|
|
|
| <div class="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4">
|
| <h2 class="text-2xl font-bold text-gray-800 mb-4">刷新Refresh Token失败的账号</h2>
|
| <details class="mb-4">
|
| <summary class="cursor-pointer text-blue-600 hover:text-blue-800">点击查看</summary>
|
| <div id="failedTokens" class="mt-2 space-y-1">
|
|
|
| </div>
|
| </details>
|
| </div>
|
|
|
| <div class="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4">
|
| <h2 class="text-2xl font-bold text-gray-800 mb-4">刷新历史</h2>
|
| <details class="mb-4">
|
| <summary class="cursor-pointer text-blue-600 hover:text-blue-800">点击查看</summary>
|
| <div id="refreshHistory" class="space-y-2">
|
|
|
| </div>
|
| </details>
|
|
|
| </div>
|
|
|
| <div id="tokenModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 hidden">
|
| <div class="relative top-20 mx-auto p-5 border w-96 shadow-lg rounded-md bg-white">
|
| <div class="mt-3">
|
| <h3 class="text-lg font-medium text-gray-900 mb-4">账号信息</h3>
|
| <form id="tokenForm">
|
| <input type="hidden" id="id">
|
| <div class="mb-4">
|
| <label class="block text-gray-700 text-sm font-bold mb-2" for="email">
|
| 邮箱
|
| </label>
|
| <input
|
| class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
|
| id="email" type="text" required placeholder="仅用于标识 之后不得修改">
|
| </div>
|
| <div class="mb-4">
|
| <label class="block text-gray-700 text-sm font-bold mb-2" for="ReToken">
|
| Refresh Token
|
| </label>
|
| <input
|
| class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
|
| id="ReToken" type="text" placeholder="可为空">
|
| </div>
|
| <div class="mb-4">
|
| <label class="block text-gray-700 text-sm font-bold mb-2" for="acToken">
|
| Access Token
|
| </label>
|
| <input
|
| class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
|
| id="AcToken" type="text" placeholder="无Refresh Token时必填">
|
| </div>
|
| <div class="mb-4">
|
| <label class="block text-gray-700 text-sm font-bold mb-2" for="PLUS">
|
| PLUS
|
| </label>
|
| <select
|
| class="shadow border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
|
| id="PLUS" required>
|
| <option value='false'>false</option>
|
| <option value='true'>true</option>
|
| </select>
|
| </div>
|
| <div class="flex items-center justify-end">
|
| <button type="button" onclick="closeModal()"
|
| class="bg-gray-500 hover:bg-gray-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline mr-2">
|
| 取消
|
| </button>
|
| <button type="submit"
|
| class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline">
|
| 保存
|
| </button>
|
| </div>
|
| </form>
|
| </div>
|
| </div>
|
| </div>
|
|
|
| </div>
|
| {% endblock %}
|
| {% block extra_js %}
|
|
|
| <script>
|
|
|
| function loadTokens() {
|
| fetch('/get_tokens')
|
| .then(response => response.json())
|
| .then(data => {
|
| tokens = data
|
| tokenTableBody.innerHTML = '';
|
| data.forEach(token => {
|
| let rt = '有'
|
| if (!token.refresh_token) {
|
| rt = '无'
|
| }
|
| let recolor = token.refresh_token ? ' style="color: rgb(6, 161, 6);"' : ' style="color: rgb(204, 28, 28);"'
|
| let color = token.status ? ' style="color: rgb(6, 161, 6);"' : ' style="color: rgb(204, 28, 28);"'
|
| const row = document.createElement('tr');
|
| row.innerHTML = `
|
| <td class="truncate">${token.email}</td>
|
| <td class="truncate" ${recolor}>${rt}</td>
|
| <td class="truncate" ${color}>${token.status ? '有效' : '失效'}</td>
|
| <td>
|
| <button onclick="editUser('${token.email}')" class="text-blue-600 hover:text-blue-900 mr-4">编辑</button>
|
| <button onclick="deleteUser('${token.email}')" class="text-red-600 hover:text-red-900">删除</button>
|
| </td>
|
| `;
|
| tokenTableBody.appendChild(row);
|
| });
|
| })
|
| .catch(error => {
|
| console.error('加载 Tokens 失败:', error);
|
| showStatus('加载 Tokens 失败', 'error');
|
| });
|
| }
|
|
|
| let tokens = []
|
| const modal = document.getElementById('tokenModal');
|
| const tokenForm = document.getElementById('tokenForm');
|
| const addTokenBtn = document.getElementById('addTokenBtn');
|
|
|
|
|
| function openModal(email = null) {
|
| const form = document.getElementById('tokenForm');
|
| if (email) {
|
| document.getElementById('id').value = 'id'
|
| document.getElementById('email').value = email.email;
|
| document.getElementById('ReToken').value = email.refresh_token;
|
| document.getElementById('AcToken').value = email.access_token;
|
| document.getElementById('PLUS').value = email.PLUS;
|
| } else {
|
| form.reset();
|
| document.getElementById('id').value = ''
|
| }
|
| modal.classList.remove('hidden');
|
| }
|
|
|
|
|
| function closeModal() {
|
| modal.classList.add('hidden');
|
| }
|
|
|
|
|
| function editUser(email) {
|
| const tkemail = tokens.find(u => u.email === email);
|
| if (tkemail) {
|
| openModal(tkemail);
|
| }
|
| }
|
|
|
|
|
| function deleteUser(email) {
|
| if (confirm('确定要删除这个账户吗?')) {
|
| fetch(`/api/tokens/${email}`, {
|
| method: 'DELETE'
|
| })
|
| .then(response => response.json())
|
| .then(data => {
|
| if (data.success) {
|
| loadTokens();
|
| }
|
| })
|
| .catch(error => console.error('Error:', error));
|
| }
|
| }
|
|
|
|
|
| tokenForm.addEventListener('submit', function (e) {
|
| e.preventDefault();
|
| const Id = document.getElementById('id').value;
|
| const tkemail = document.getElementById('email').value
|
| const retoken = document.getElementById('ReToken').value
|
| const actoken = document.getElementById('AcToken').value
|
| const plus = document.getElementById('PLUS').value
|
| if(!(retoken || actoken)){
|
| alert('必须有一个Refresh Token或Access Token')
|
| return
|
| }
|
| const tkData = {
|
| email: document.getElementById('email').value,
|
| ReToken: retoken,
|
| AcToken: actoken,
|
| PLUS: plus
|
| };
|
|
|
| const url = Id ? `/api/tokens/${tkemail}` : '/api/tokens';
|
| const method = Id ? 'PUT' : 'POST';
|
|
|
| fetch(url, {
|
| method: method,
|
| headers: {
|
| 'Content-Type': 'application/json'
|
| },
|
| body: JSON.stringify(tkData)
|
| })
|
| .then(response => response.json())
|
| .then(data => {
|
| if (data.success) {
|
| closeModal();
|
| loadTokens();
|
| }
|
| })
|
| .catch(error => {
|
| console.error('Error:', error);
|
| });
|
|
|
| });
|
|
|
|
|
| addTokenBtn.addEventListener('click', () => openModal());
|
| </script>
|
| <script>
|
| document.addEventListener('DOMContentLoaded', function () {
|
| const tokenTableBody = document.getElementById('tokenTableBody');
|
| const refreshBtn = document.getElementById('refreshBtn');
|
| const statusMessage = document.getElementById('statusMessage');
|
| const autoRefreshToggle = document.getElementById('autoRefreshToggle');
|
| const refreshInterval = document.getElementById('refreshInterval');
|
| const nextRefreshTime = document.getElementById('nextRefreshTime');
|
| const refreshHistory = document.getElementById('refreshHistory');
|
|
|
| function loadTokens() {
|
| fetch('/get_tokens')
|
| .then(response => response.json())
|
| .then(data => {
|
| tokens = data
|
| tokenTableBody.innerHTML = '';
|
| data.forEach(token => {
|
| let rt = '有'
|
| if (!token.refresh_token) {
|
| rt = '无'
|
| }
|
| let recolor = token.refresh_token ? ' style="color: rgb(6, 161, 6);"' : ' style="color: rgb(204, 28, 28);"'
|
| let color = token.status ? ' style="color: rgb(6, 161, 6);"' : ' style="color: rgb(204, 28, 28);"'
|
| const row = document.createElement('tr');
|
| row.innerHTML = `
|
| <td class="truncate">${token.email}</td>
|
| <td class="truncate" ${recolor}>${rt}</td>
|
| <td class="truncate" ${color}>${token.status ? '有效' : '失效'}</td>
|
| <td>
|
| <button onclick="editUser('${token.email}')" class="text-blue-600 hover:text-blue-900 mr-4">编辑</button>
|
| <button onclick="deleteUser('${token.email}')" class="text-red-600 hover:text-red-900">删除</button>
|
| </td>
|
| `;
|
| tokenTableBody.appendChild(row);
|
| });
|
| })
|
| .catch(error => {
|
| console.error('加载 Tokens 失败:', error);
|
| showStatus('加载 Tokens 失败', 'error');
|
| });
|
| }
|
|
|
| function loadFailedTokens() {
|
| fetch('/get_failed_tokens')
|
| .then(response => response.json())
|
| .then(data => {
|
| const failedTokensDiv = document.getElementById('failedTokens');
|
| failedTokensDiv.innerHTML = '';
|
| data.forEach(item => {
|
| const email = item.email;
|
| const tokenItem = document.createElement('div');
|
| tokenItem.className = 'bg-red-100 p-2 rounded';
|
| tokenItem.textContent = email;
|
| failedTokensDiv.appendChild(tokenItem);
|
| });
|
| })
|
| .catch(error => {
|
| console.error('加载失败Refresh Token 失败:', error);
|
| showStatus('加载失败的 Tokens 失败', 'error');
|
| });
|
| }
|
|
|
| function updateAutoRefreshUI(config) {
|
| autoRefreshToggle.checked = config.auto_refresh_enabled;
|
| refreshInterval.value = config.refresh_interval_days;
|
| if (config.next_refresh_time) {
|
| nextRefreshTime.textContent = `下次刷新时间: ${new Date(config.next_refresh_time).toLocaleString()}`;
|
| } else {
|
| nextRefreshTime.textContent = '自动刷新已关闭';
|
| }
|
| }
|
|
|
| function loadAutoRefreshConfig() {
|
| fetch('/get_auto_refresh_config')
|
| .then(response => response.json())
|
| .then(config => {
|
| updateAutoRefreshUI(config);
|
| })
|
| .catch(error => {
|
| console.error('加载自动刷新配置失败:', error);
|
| showStatus('加载自动刷新配置失败', 'error');
|
| });
|
| }
|
|
|
| function saveAutoRefreshConfig() {
|
| fetch('/set_auto_refresh', {
|
| method: 'POST',
|
| headers: {
|
| 'Content-Type': 'application/json'
|
| },
|
| body: JSON.stringify({
|
| enabled: autoRefreshToggle.checked,
|
| interval: parseInt(refreshInterval.value)
|
| })
|
| })
|
| .then(response => response.json())
|
| .then(data => {
|
| if (data.status === 'success') {
|
| showStatus('自动刷新设置已更新', 'success');
|
| loadAutoRefreshConfig();
|
| } else {
|
| showStatus('更新自动刷新设置失败', 'error');
|
| }
|
| })
|
| .catch(error => {
|
| showStatus('更新自动刷新设置失败', 'error');
|
| });
|
| }
|
|
|
| function refreshTokens() {
|
| refreshBtn.disabled = true;
|
| fetch('/refresh_tokens', {
|
| method: 'POST'
|
| })
|
| .then(response => response.json())
|
| .then(data => {
|
| if (data.status === 'success') {
|
| refreshBtn.disabled = false;
|
| showStatus('Tokens 刷新成功', 'success');
|
| loadTokens();
|
| loadRefreshHistory();
|
| loadAutoRefreshConfig();
|
| loadFailedTokens();
|
| } else {
|
| refreshBtn.disabled = false;
|
| showStatus('Tokens 刷新失败: ' + data.message, 'error');
|
| }
|
| })
|
| .catch(error => {
|
| refreshBtn.disabled = false;
|
| showStatus('Tokens 刷新失败', 'error');
|
| });
|
| }
|
|
|
| function loadRefreshHistory() {
|
| fetch('/refresh_history')
|
| .then(response => response.json())
|
| .then(data => {
|
| if (data.status === 'success') {
|
| refreshHistory.innerHTML = '';
|
| data.history.forEach(item => {
|
| const historyItem = document.createElement('div');
|
| historyItem.className = 'bg-gray-100 p-2 rounded';
|
| historyItem.innerHTML = `
|
| <p><strong>刷新时间:</strong> ${new Date(item.timestamp).toLocaleString()}</p>
|
| <p><strong>Token 数量:</strong> ${item.token_count}</p>
|
| `;
|
| refreshHistory.appendChild(historyItem);
|
| });
|
| }
|
| })
|
| .catch(error => {
|
| console.error('加载刷新历史失败:', error);
|
| showStatus('加载刷新历史失败', 'error');
|
| });
|
| }
|
|
|
| function showStatus(message, status) {
|
| statusMessage.textContent = message;
|
| statusMessage.className = status === 'success' ? 'text-green-600' : 'text-red-600';
|
| setTimeout(() => {
|
| statusMessage.textContent = '';
|
| }, 3000);
|
| }
|
|
|
|
|
| refreshBtn.addEventListener('click', refreshTokens);
|
| autoRefreshToggle.addEventListener('change', saveAutoRefreshConfig);
|
| refreshInterval.addEventListener('change', saveAutoRefreshConfig);
|
|
|
|
|
|
|
| loadTokens();
|
| loadAutoRefreshConfig();
|
| loadRefreshHistory();
|
| loadFailedTokens();
|
| });
|
| </script>
|
| {% endblock %}
|
|
|