Add files via upload

This commit is contained in:
公明
2025-11-13 23:41:00 +08:00
committed by GitHub
parent b63fd24b18
commit 6f0044b6fd
11 changed files with 1163 additions and 97 deletions
+88 -9
View File
@@ -56,7 +56,7 @@ body {
header {
background: var(--primary-color);
color: white;
padding: 24px 32px;
padding: 16px 24px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
flex-shrink: 0;
}
@@ -289,7 +289,7 @@ header {
flex-direction: column;
flex: 1;
min-width: 0;
background: var(--bg-primary);
background: var(--bg-secondary);
overflow: hidden;
height: 100%;
}
@@ -755,6 +755,67 @@ header {
transform: translateY(0);
}
/* 登录遮罩 */
.login-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.6);
backdrop-filter: blur(6px);
display: none;
align-items: center;
justify-content: center;
z-index: 1200;
padding: 24px;
}
.login-card {
width: 100%;
max-width: 360px;
background: var(--bg-primary);
border-radius: 12px;
padding: 32px 28px;
box-shadow: var(--shadow-lg);
border: 1px solid var(--border-color);
display: flex;
flex-direction: column;
gap: 20px;
}
.login-header h2 {
margin: 0;
font-size: 1.5rem;
color: var(--text-primary);
}
.login-subtitle {
margin: 8px 0 0 0;
font-size: 0.9375rem;
color: var(--text-secondary);
}
.login-form {
display: flex;
flex-direction: column;
gap: 16px;
}
.login-error {
color: var(--error-color);
background: rgba(220, 53, 69, 0.08);
border: 1px solid rgba(220, 53, 69, 0.4);
border-radius: 6px;
padding: 10px 12px;
font-size: 0.875rem;
}
.login-submit {
width: 100%;
justify-content: center;
display: inline-flex;
align-items: center;
gap: 8px;
}
/* 模态框样式 */
.modal {
display: none;
@@ -1251,9 +1312,12 @@ header {
display: none;
align-items: center;
gap: 12px;
padding: 12px 20px;
background: rgba(0, 102, 255, 0.06);
border-bottom: 1px solid rgba(0, 102, 255, 0.15);
padding: 10px 16px;
margin: 12px 0;
background: var(--bg-primary);
border: 1px solid rgba(0, 102, 255, 0.15);
border-radius: 10px;
box-shadow: var(--shadow-sm);
color: var(--text-primary);
}
@@ -1261,20 +1325,22 @@ header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
gap: 12px;
background: var(--bg-primary);
border: 1px solid rgba(0, 102, 255, 0.2);
border-radius: 8px;
padding: 8px 12px;
flex: 1;
min-width: 0;
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.03);
}
.active-task-info {
display: flex;
align-items: center;
gap: 8px;
gap: 6px;
min-width: 0;
flex-wrap: wrap;
}
.active-task-status {
@@ -1288,7 +1354,7 @@ header {
}
.active-task-message {
font-size: 0.875rem;
font-size: 0.85rem;
color: var(--text-primary);
overflow: hidden;
text-overflow: ellipsis;
@@ -1299,7 +1365,7 @@ header {
.active-task-actions {
display: flex;
align-items: center;
gap: 10px;
gap: 8px;
flex-shrink: 0;
}
@@ -1409,6 +1475,19 @@ header {
box-shadow: 0 0 0 3px rgba(220, 53, 69, 0.2);
}
.form-actions {
display: flex;
justify-content: flex-end;
gap: 12px;
margin-top: 4px;
}
.password-hint {
font-size: 0.8125rem;
color: var(--text-muted);
margin-top: 8px;
}
.tools-controls {
display: flex;
flex-direction: column;
+371 -30
View File
@@ -1,3 +1,9 @@
const AUTH_STORAGE_KEY = 'cyberstrike-auth';
let authToken = null;
let authTokenExpiry = null;
let authPromise = null;
let authPromiseResolvers = [];
let isAppInitialized = false;
// 当前对话ID
let currentConversationId = null;
@@ -7,6 +13,280 @@ const progressTaskState = new Map();
let activeTaskInterval = null;
const ACTIVE_TASK_REFRESH_INTERVAL = 10000; // 10秒检查一次,提供更实时的任务状态反馈
function isTokenValid() {
return !!authToken && authTokenExpiry instanceof Date && authTokenExpiry.getTime() > Date.now();
}
function saveAuth(token, expiresAt) {
const expiry = expiresAt instanceof Date ? expiresAt : new Date(expiresAt);
authToken = token;
authTokenExpiry = expiry;
try {
localStorage.setItem(AUTH_STORAGE_KEY, JSON.stringify({
token,
expiresAt: expiry.toISOString(),
}));
} catch (error) {
console.warn('无法持久化认证信息:', error);
}
}
function clearAuthStorage() {
authToken = null;
authTokenExpiry = null;
try {
localStorage.removeItem(AUTH_STORAGE_KEY);
} catch (error) {
console.warn('无法清除认证信息:', error);
}
}
function loadAuthFromStorage() {
try {
const raw = localStorage.getItem(AUTH_STORAGE_KEY);
if (!raw) {
return false;
}
const stored = JSON.parse(raw);
if (!stored.token || !stored.expiresAt) {
clearAuthStorage();
return false;
}
const expiry = new Date(stored.expiresAt);
if (Number.isNaN(expiry.getTime())) {
clearAuthStorage();
return false;
}
authToken = stored.token;
authTokenExpiry = expiry;
return isTokenValid();
} catch (error) {
console.error('读取认证信息失败:', error);
clearAuthStorage();
return false;
}
}
function resolveAuthPromises(success) {
authPromiseResolvers.forEach(resolve => resolve(success));
authPromiseResolvers = [];
authPromise = null;
}
function showLoginOverlay(message = '') {
const overlay = document.getElementById('login-overlay');
const errorBox = document.getElementById('login-error');
const passwordInput = document.getElementById('login-password');
if (!overlay) {
return;
}
overlay.style.display = 'flex';
if (errorBox) {
if (message) {
errorBox.textContent = message;
errorBox.style.display = 'block';
} else {
errorBox.textContent = '';
errorBox.style.display = 'none';
}
}
setTimeout(() => {
if (passwordInput) {
passwordInput.focus();
}
}, 100);
}
function hideLoginOverlay() {
const overlay = document.getElementById('login-overlay');
const errorBox = document.getElementById('login-error');
const passwordInput = document.getElementById('login-password');
if (overlay) {
overlay.style.display = 'none';
}
if (errorBox) {
errorBox.textContent = '';
errorBox.style.display = 'none';
}
if (passwordInput) {
passwordInput.value = '';
}
}
function ensureAuthPromise() {
if (!authPromise) {
authPromise = new Promise(resolve => {
authPromiseResolvers.push(resolve);
});
}
return authPromise;
}
async function ensureAuthenticated() {
if (isTokenValid()) {
return true;
}
showLoginOverlay();
await ensureAuthPromise();
return true;
}
function handleUnauthorized({ message = '认证已过期,请重新登录', silent = false } = {}) {
clearAuthStorage();
authPromise = null;
authPromiseResolvers = [];
if (!silent) {
showLoginOverlay(message);
} else {
showLoginOverlay();
}
return false;
}
async function apiFetch(url, options = {}) {
await ensureAuthenticated();
const opts = { ...options };
const headers = new Headers(options && options.headers ? options.headers : undefined);
if (authToken && !headers.has('Authorization')) {
headers.set('Authorization', `Bearer ${authToken}`);
}
opts.headers = headers;
const response = await fetch(url, opts);
if (response.status === 401) {
handleUnauthorized();
throw new Error('未授权访问');
}
return response;
}
async function submitLogin(event) {
event.preventDefault();
const passwordInput = document.getElementById('login-password');
const errorBox = document.getElementById('login-error');
const submitBtn = document.querySelector('.login-submit');
if (!passwordInput) {
return;
}
const password = passwordInput.value.trim();
if (!password) {
if (errorBox) {
errorBox.textContent = '请输入密码';
errorBox.style.display = 'block';
}
return;
}
if (submitBtn) {
submitBtn.disabled = true;
}
try {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ password }),
});
const result = await response.json().catch(() => ({}));
if (!response.ok || !result.token) {
if (errorBox) {
errorBox.textContent = result.error || '登录失败,请检查密码';
errorBox.style.display = 'block';
}
return;
}
saveAuth(result.token, result.expires_at);
hideLoginOverlay();
resolveAuthPromises(true);
if (!isAppInitialized) {
await bootstrapApp();
} else {
await refreshAppData();
}
} catch (error) {
console.error('登录失败:', error);
if (errorBox) {
errorBox.textContent = '登录失败,请稍后重试';
errorBox.style.display = 'block';
}
} finally {
if (submitBtn) {
submitBtn.disabled = false;
}
}
}
async function refreshAppData(showTaskErrors = false) {
await Promise.allSettled([
loadConversations(),
loadActiveTasks(showTaskErrors),
]);
}
async function bootstrapApp() {
if (!isAppInitialized) {
initializeChatUI();
isAppInitialized = true;
}
await refreshAppData();
}
function initializeChatUI() {
const chatInputEl = document.getElementById('chat-input');
if (chatInputEl) {
chatInputEl.style.height = '44px';
}
const messagesDiv = document.getElementById('chat-messages');
if (messagesDiv && messagesDiv.childElementCount === 0) {
addMessage('assistant', '系统已就绪。请输入您的测试需求,系统将自动执行相应的安全测试。');
}
loadActiveTasks(true);
if (activeTaskInterval) {
clearInterval(activeTaskInterval);
}
activeTaskInterval = setInterval(() => loadActiveTasks(), ACTIVE_TASK_REFRESH_INTERVAL);
}
function setupLoginUI() {
const loginForm = document.getElementById('login-form');
if (loginForm) {
loginForm.addEventListener('submit', submitLogin);
}
}
async function initializeApp() {
setupLoginUI();
const hasStoredAuth = loadAuthFromStorage();
if (hasStoredAuth && isTokenValid()) {
try {
const response = await apiFetch('/api/auth/validate', {
method: 'GET',
});
if (response.ok) {
hideLoginOverlay();
resolveAuthPromises(true);
await bootstrapApp();
return;
}
} catch (error) {
console.warn('本地会话已失效,需重新登录');
}
}
clearAuthStorage();
showLoginOverlay();
}
document.addEventListener('DOMContentLoaded', initializeApp);
function registerProgressTask(progressId, conversationId = null) {
const state = progressTaskState.get(progressId) || {};
state.conversationId = conversationId !== undefined && conversationId !== null
@@ -45,7 +325,7 @@ function finalizeProgressTask(progressId, finalLabel = '已完成') {
}
async function requestCancel(conversationId) {
const response = await fetch('/api/agent-loop/cancel', {
const response = await apiFetch('/api/agent-loop/cancel', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@@ -81,7 +361,7 @@ async function sendMessage() {
let mcpExecutionIds = [];
try {
const response = await fetch('/api/agent-loop/stream', {
const response = await apiFetch('/api/agent-loop/stream', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@@ -950,7 +1230,7 @@ chatInput.addEventListener('keydown', function(e) {
// 显示MCP调用详情
async function showMCPDetail(executionId) {
try {
const response = await fetch(`/api/monitor/execution/${executionId}`);
const response = await apiFetch(`/api/monitor/execution/${executionId}`);
const exec = await response.json();
if (response.ok) {
@@ -1064,7 +1344,7 @@ function startNewConversation() {
// 加载对话列表
async function loadConversations() {
try {
const response = await fetch('/api/conversations?limit=50');
const response = await apiFetch('/api/conversations?limit=50');
const conversations = await response.json();
const listContainer = document.getElementById('conversations-list');
@@ -1179,7 +1459,7 @@ async function loadConversations() {
// 加载对话
async function loadConversation(conversationId) {
try {
const response = await fetch(`/api/conversations/${conversationId}`);
const response = await apiFetch(`/api/conversations/${conversationId}`);
const conversation = await response.json();
if (!response.ok) {
@@ -1230,7 +1510,7 @@ async function deleteConversation(conversationId) {
}
try {
const response = await fetch(`/api/conversations/${conversationId}`, {
const response = await apiFetch(`/api/conversations/${conversationId}`, {
method: 'DELETE'
});
@@ -1268,7 +1548,7 @@ function updateActiveConversation() {
async function loadActiveTasks(showErrors = false) {
const bar = document.getElementById('active-tasks-bar');
try {
const response = await fetch('/api/agent-loop/tasks');
const response = await apiFetch('/api/agent-loop/tasks');
const result = await response.json().catch(() => ({}));
if (!response.ok) {
@@ -1386,7 +1666,7 @@ window.onclick = function(event) {
// 加载配置
async function loadConfig() {
try {
const response = await fetch('/api/config');
const response = await apiFetch('/api/config');
if (!response.ok) {
throw new Error('获取配置失败');
}
@@ -1520,7 +1800,7 @@ async function applySettings() {
});
// 更新配置
const updateResponse = await fetch('/api/config', {
const updateResponse = await apiFetch('/api/config', {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
@@ -1534,7 +1814,7 @@ async function applySettings() {
}
// 应用配置
const applyResponse = await fetch('/api/config/apply', {
const applyResponse = await apiFetch('/api/config/apply', {
method: 'POST'
});
@@ -1551,24 +1831,85 @@ async function applySettings() {
}
}
// 页面加载时初始化
document.addEventListener('DOMContentLoaded', function() {
// 加载对话列表
loadConversations();
// 初始化 textarea 高度
const chatInput = document.getElementById('chat-input');
if (chatInput) {
chatInput.style.height = '44px';
}
// 添加欢迎消息
addMessage('assistant', '系统已就绪。请输入您的测试需求,系统将自动执行相应的安全测试。');
loadActiveTasks(true);
if (activeTaskInterval) {
clearInterval(activeTaskInterval);
}
activeTaskInterval = setInterval(() => loadActiveTasks(), ACTIVE_TASK_REFRESH_INTERVAL);
});
function resetPasswordForm() {
const currentInput = document.getElementById('auth-current-password');
const newInput = document.getElementById('auth-new-password');
const confirmInput = document.getElementById('auth-confirm-password');
[currentInput, newInput, confirmInput].forEach(input => {
if (input) {
input.value = '';
input.classList.remove('error');
}
});
}
async function changePassword() {
const currentInput = document.getElementById('auth-current-password');
const newInput = document.getElementById('auth-new-password');
const confirmInput = document.getElementById('auth-confirm-password');
const submitBtn = document.querySelector('.change-password-submit');
[currentInput, newInput, confirmInput].forEach(input => input && input.classList.remove('error'));
const currentPassword = currentInput?.value.trim() || '';
const newPassword = newInput?.value.trim() || '';
const confirmPassword = confirmInput?.value.trim() || '';
let hasError = false;
if (!currentPassword) {
currentInput?.classList.add('error');
hasError = true;
}
if (!newPassword || newPassword.length < 8) {
newInput?.classList.add('error');
hasError = true;
}
if (newPassword !== confirmPassword) {
confirmInput?.classList.add('error');
hasError = true;
}
if (hasError) {
alert('请正确填写当前密码和新密码,新密码至少 8 位且需要两次输入一致。');
return;
}
if (submitBtn) {
submitBtn.disabled = true;
}
try {
const response = await apiFetch('/api/auth/change-password', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
oldPassword: currentPassword,
newPassword: newPassword
})
});
const result = await response.json().catch(() => ({}));
if (!response.ok) {
throw new Error(result.error || '修改密码失败');
}
alert('密码已更新,请使用新密码重新登录。');
resetPasswordForm();
handleUnauthorized({ message: '密码已更新,请使用新密码重新登录。', silent: false });
closeSettings();
} catch (error) {
console.error('修改密码失败:', error);
alert('修改密码失败: ' + error.message);
} finally {
if (submitBtn) {
submitBtn.disabled = false;
}
}
}
+41
View File
@@ -7,6 +7,23 @@
<link rel="stylesheet" href="/static/css/style.css">
</head>
<body>
<div id="login-overlay" class="login-overlay" style="display: none;">
<div class="login-card">
<div class="login-header">
<h2>登录 CyberStrike</h2>
<p class="login-subtitle">请输入配置中的访问密码</p>
</div>
<form id="login-form" class="login-form">
<div class="form-group">
<label for="login-password">密码</label>
<input type="password" id="login-password" placeholder="输入登录密码" required autocomplete="current-password" />
</div>
<div id="login-error" class="login-error" role="alert" style="display: none;"></div>
<button type="submit" class="btn-primary login-submit">登录</button>
</form>
</div>
</div>
<div class="container">
<header>
<div class="header-content">
@@ -106,6 +123,30 @@
</div>
</div>
</div>
<!-- 安全设置 -->
<div class="settings-section">
<h3>安全设置</h3>
<div class="settings-form">
<div class="form-group">
<label for="auth-current-password">当前密码</label>
<input type="password" id="auth-current-password" placeholder="输入当前登录密码" autocomplete="current-password" />
</div>
<div class="form-group">
<label for="auth-new-password">新密码</label>
<input type="password" id="auth-new-password" placeholder="设置新密码(至少 8 位)" autocomplete="new-password" />
</div>
<div class="form-group">
<label for="auth-confirm-password">确认新密码</label>
<input type="password" id="auth-confirm-password" placeholder="再次输入新密码" autocomplete="new-password" />
</div>
<div class="form-actions">
<button class="btn-secondary" type="button" onclick="resetPasswordForm()">清空</button>
<button class="btn-primary change-password-submit" type="button" onclick="changePassword()">修改密码</button>
</div>
<p class="password-hint">修改密码后,需要使用新密码重新登录。</p>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn-secondary" onclick="closeSettings()">取消</button>