mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-06-06 06:13:58 +02:00
Add files via upload
This commit is contained in:
@@ -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);
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user