|
|
{% 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 %}
|
|
|
|