mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-03-31 16:20:28 +02:00
Add files via upload
This commit is contained in:
331
web/static/js/auth.js
Normal file
331
web/static/js/auth.js
Normal file
@@ -0,0 +1,331 @@
|
||||
const AUTH_STORAGE_KEY = 'cyberstrike-auth';
|
||||
let authToken = null;
|
||||
let authTokenExpiry = null;
|
||||
let authPromise = null;
|
||||
let authPromiseResolvers = [];
|
||||
let isAppInitialized = false;
|
||||
|
||||
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 getStatusText(status) {
|
||||
const statusMap = {
|
||||
'pending': '等待中',
|
||||
'running': '执行中',
|
||||
'completed': '已完成',
|
||||
'failed': '失败'
|
||||
};
|
||||
return statusMap[status] || status;
|
||||
}
|
||||
|
||||
function formatDuration(ms) {
|
||||
const seconds = Math.floor(ms / 1000);
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const hours = Math.floor(minutes / 60);
|
||||
|
||||
if (hours > 0) {
|
||||
return `${hours}小时${minutes % 60}分钟`;
|
||||
} else if (minutes > 0) {
|
||||
return `${minutes}分钟${seconds % 60}秒`;
|
||||
} else {
|
||||
return `${seconds}秒`;
|
||||
}
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
function formatMarkdown(text) {
|
||||
const sanitizeConfig = {
|
||||
ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'u', 's', 'code', 'pre', 'blockquote', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'ul', 'ol', 'li', 'a', 'img', 'table', 'thead', 'tbody', 'tr', 'th', 'td', 'hr'],
|
||||
ALLOWED_ATTR: ['href', 'title', 'alt', 'src', 'class'],
|
||||
ALLOW_DATA_ATTR: false,
|
||||
};
|
||||
|
||||
if (typeof DOMPurify !== 'undefined') {
|
||||
if (typeof marked !== 'undefined' && !/<[a-z][\s\S]*>/i.test(text)) {
|
||||
try {
|
||||
marked.setOptions({
|
||||
breaks: true,
|
||||
gfm: true,
|
||||
});
|
||||
let parsedContent = marked.parse(text);
|
||||
return DOMPurify.sanitize(parsedContent, sanitizeConfig);
|
||||
} catch (e) {
|
||||
console.error('Markdown 解析失败:', e);
|
||||
return DOMPurify.sanitize(text, sanitizeConfig);
|
||||
}
|
||||
} else {
|
||||
return DOMPurify.sanitize(text, sanitizeConfig);
|
||||
}
|
||||
} else if (typeof marked !== 'undefined') {
|
||||
try {
|
||||
marked.setOptions({
|
||||
breaks: true,
|
||||
gfm: true,
|
||||
});
|
||||
return marked.parse(text);
|
||||
} catch (e) {
|
||||
console.error('Markdown 解析失败:', e);
|
||||
return escapeHtml(text).replace(/\n/g, '<br>');
|
||||
}
|
||||
} else {
|
||||
return escapeHtml(text).replace(/\n/g, '<br>');
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
1704
web/static/js/chat.js
Normal file
1704
web/static/js/chat.js
Normal file
File diff suppressed because it is too large
Load Diff
1220
web/static/js/monitor.js
Normal file
1220
web/static/js/monitor.js
Normal file
File diff suppressed because it is too large
Load Diff
1226
web/static/js/settings.js
Normal file
1226
web/static/js/settings.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -377,7 +377,10 @@
|
||||
<script src="https://cdn.jsdelivr.net/npm/dagre@0.8.5/dist/dagre.min.js"></script>
|
||||
<!-- dagre layout for hierarchical layout -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/cytoscape-dagre@2.5.0/cytoscape-dagre.min.js"></script>
|
||||
<script src="/static/js/app.js"></script>
|
||||
<script src="/static/js/auth.js"></script>
|
||||
<script src="/static/js/monitor.js"></script>
|
||||
<script src="/static/js/chat.js"></script>
|
||||
<script src="/static/js/settings.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user