|
|
const commandInput = document.getElementById('command');
|
|
|
const output = document.getElementById('output');
|
|
|
const history = document.getElementById('history');
|
|
|
const executeButton = document.getElementById('executeButton');
|
|
|
const loadingIndicator = document.getElementById('loadingIndicator');
|
|
|
|
|
|
commandInput.addEventListener('keypress', function (event) {
|
|
|
if (event.key === 'Enter') {
|
|
|
executeCommand();
|
|
|
}
|
|
|
});
|
|
|
|
|
|
async function executeCommand() {
|
|
|
const command = commandInput.value;
|
|
|
if (!command.trim()) return;
|
|
|
|
|
|
showLoading(true);
|
|
|
clearOutput();
|
|
|
appendToOutput('$ ' + command, 'text-blue-400');
|
|
|
appendToOutput('正在执行命令...', 'text-yellow-400');
|
|
|
|
|
|
try {
|
|
|
let response = await fetch('/api/execute', {
|
|
|
method: 'POST',
|
|
|
headers: {
|
|
|
'Content-Type': 'application/json',
|
|
|
'Authorization': `Bearer ${localStorage.getItem('token')}`
|
|
|
},
|
|
|
body: JSON.stringify({ command })
|
|
|
});
|
|
|
|
|
|
if (response.status === 403) {
|
|
|
const refreshed = await refreshToken();
|
|
|
if (refreshed) {
|
|
|
response = await fetch('/api/execute', {
|
|
|
method: 'POST',
|
|
|
headers: {
|
|
|
'Content-Type': 'application/json',
|
|
|
'Authorization': `Bearer ${localStorage.getItem('token')}`
|
|
|
},
|
|
|
body: JSON.stringify({ command })
|
|
|
});
|
|
|
} else {
|
|
|
throw new Error('Token 刷新失败');
|
|
|
}
|
|
|
}
|
|
|
|
|
|
if (!response.ok) {
|
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
|
}
|
|
|
|
|
|
const data = await response.json();
|
|
|
clearOutput();
|
|
|
appendToOutput('$ ' + command, 'text-blue-400');
|
|
|
appendToOutput(data.output || data.error || '命令执行成功,但没有输出。', 'text-green-400');
|
|
|
commandInput.value = '';
|
|
|
loadCommandHistory();
|
|
|
} catch (error) {
|
|
|
console.error('执行命令时出错:', error);
|
|
|
clearOutput();
|
|
|
appendToOutput('$ ' + command, 'text-blue-400');
|
|
|
appendToOutput('错误: ' + (error.message || '未知错误'), 'text-red-500');
|
|
|
if (error.message.includes('Token 刷新失败')) {
|
|
|
showNotification('访问被拒绝。请重新登录。', 'error');
|
|
|
localStorage.removeItem('token');
|
|
|
localStorage.removeItem('username');
|
|
|
localStorage.removeItem('password');
|
|
|
checkLoginStatus();
|
|
|
}
|
|
|
} finally {
|
|
|
showLoading(false);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
function clearOutput() {
|
|
|
output.innerHTML = '';
|
|
|
}
|
|
|
|
|
|
function appendToOutput(text, className = 'text-green-400') {
|
|
|
const lines = text.split('\n');
|
|
|
lines.forEach((line, index) => {
|
|
|
const p = document.createElement('p');
|
|
|
p.className = className;
|
|
|
p.textContent = line;
|
|
|
output.appendChild(p);
|
|
|
});
|
|
|
output.scrollTop = output.scrollHeight;
|
|
|
}
|
|
|
|
|
|
async function loadCommandHistory() {
|
|
|
try {
|
|
|
const response = await fetch('/api/command-history', {
|
|
|
headers: {
|
|
|
'Authorization': `Bearer ${localStorage.getItem('token')}`
|
|
|
}
|
|
|
});
|
|
|
const data = await response.json();
|
|
|
history.innerHTML = '';
|
|
|
data.reverse().forEach(item => {
|
|
|
const li = document.createElement('li');
|
|
|
li.innerHTML = `
|
|
|
<div class="flex justify-between items-center p-2 hover:bg-gray-700 rounded transition-colors duration-200">
|
|
|
<span class="font-semibold text-blue-400">${filterXSS(item.command)}</span>
|
|
|
<span class="text-sm text-gray-500">${new Date(item.timestamp).toLocaleString()}</span>
|
|
|
</div>
|
|
|
`;
|
|
|
li.className = 'cursor-pointer';
|
|
|
li.onclick = () => {
|
|
|
commandInput.value = item.command;
|
|
|
commandInput.focus();
|
|
|
};
|
|
|
history.appendChild(li);
|
|
|
});
|
|
|
} catch (error) {
|
|
|
console.error('加载命令历史失败:', error);
|
|
|
showNotification('加载命令历史失败', 'error');
|
|
|
}
|
|
|
}
|
|
|
|
|
|
function showLoading(isLoading) {
|
|
|
loadingIndicator.style.display = isLoading ? 'block' : 'none';
|
|
|
executeButton.disabled = isLoading;
|
|
|
executeButton.classList.toggle('opacity-50', isLoading);
|
|
|
}
|
|
|
|
|
|
async function login() {
|
|
|
const username = document.getElementById('username').value;
|
|
|
const password = document.getElementById('password').value;
|
|
|
try {
|
|
|
const response = await fetch('/api/login', {
|
|
|
method: 'POST',
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
body: JSON.stringify({ username, password })
|
|
|
});
|
|
|
const data = await response.json();
|
|
|
if (response.ok) {
|
|
|
localStorage.setItem('token', data.token);
|
|
|
localStorage.setItem('username', username);
|
|
|
localStorage.setItem('password', password);
|
|
|
document.getElementById('loginForm').style.display = 'none';
|
|
|
document.getElementById('commandInterface').style.display = 'block';
|
|
|
loadCommandHistory();
|
|
|
showNotification('登录成功', 'success');
|
|
|
} else {
|
|
|
showNotification('登录失败: ' + data.error, 'error');
|
|
|
}
|
|
|
} catch (error) {
|
|
|
showNotification('登录错误: ' + error.message, 'error');
|
|
|
}
|
|
|
}
|
|
|
|
|
|
async function refreshToken() {
|
|
|
const username = localStorage.getItem('username');
|
|
|
const password = localStorage.getItem('password');
|
|
|
if (!username || !password) {
|
|
|
document.getElementById('loginForm').style.display = 'block';
|
|
|
document.getElementById('commandInterface').style.display = 'none';
|
|
|
return;
|
|
|
}
|
|
|
try {
|
|
|
const response = await fetch('/api/login', {
|
|
|
method: 'POST',
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
body: JSON.stringify({ username, password })
|
|
|
});
|
|
|
const data = await response.json();
|
|
|
if (response.ok) {
|
|
|
localStorage.setItem('token', data.token);
|
|
|
return true;
|
|
|
} else {
|
|
|
throw new Error(data.error);
|
|
|
}
|
|
|
} catch (error) {
|
|
|
console.error('刷新 token 失败:', error);
|
|
|
return false;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
function showNotification(message, type = 'info') {
|
|
|
const notification = document.createElement('div');
|
|
|
notification.textContent = message;
|
|
|
notification.className = `fixed top-4 right-4 p-4 rounded-lg text-white ${type === 'error' ? 'bg-red-500' : 'bg-green-500'} shadow-lg transition-opacity duration-300`;
|
|
|
document.body.appendChild(notification);
|
|
|
setTimeout(() => {
|
|
|
notification.style.opacity = '0';
|
|
|
setTimeout(() => notification.remove(), 300);
|
|
|
}, 3000);
|
|
|
}
|
|
|
|
|
|
document.getElementById('loginButton').addEventListener('click', login);
|
|
|
|
|
|
function checkLoginStatus() {
|
|
|
const token = localStorage.getItem('token');
|
|
|
if (token) {
|
|
|
document.getElementById('loginForm').style.display = 'none';
|
|
|
document.getElementById('commandInterface').style.display = 'block';
|
|
|
loadCommandHistory();
|
|
|
}
|
|
|
}
|
|
|
|
|
|
window.addEventListener('load', checkLoginStatus);
|
|
|
document.getElementById('executeButton').addEventListener('click', executeCommand);
|
|
|
|
|
|
|
|
|
function typeWriter(element, text, speed = 50) {
|
|
|
let i = 0;
|
|
|
function type() {
|
|
|
if (i < text.length) {
|
|
|
element.textContent += text.charAt(i);
|
|
|
i++;
|
|
|
setTimeout(type, speed);
|
|
|
}
|
|
|
}
|
|
|
type();
|
|
|
}
|
|
|
|
|
|
|
|
|
window.addEventListener('load', () => {
|
|
|
const title = document.querySelector('h1');
|
|
|
title.textContent = '';
|
|
|
typeWriter(title, 'Web 命令执行器', 100);
|
|
|
}); |