diff --git a/index.html b/index.html index 67eb9f7..09c44ea 100644 --- a/index.html +++ b/index.html @@ -812,11 +812,12 @@ Login Overlay (BIZ-110)
🔐 登录验证
双色球号码生成系统
-
请输入访问令牌(LOTTO_API_TOKEN)以继续
- +
请输入账号密码以继续(BIZ-129 方案 C)
+ + -
令牌保存在浏览器 localStorage,仅本机有效。
+
会话存于 HttpOnly cookie(30 天),XSS 不可读。
@@ -998,7 +999,7 @@ const state = { }; // ============================================================ -// Login Page (BIZ-110) +// Login Page (BIZ-129 方案 C:username + password + cookie session) // ============================================================ function showLoginPage(reason) { const overlay = document.getElementById('loginOverlay'); @@ -1010,14 +1011,17 @@ function showLoginPage(reason) { // 提示信息 const hint = document.getElementById('loginHint'); if (hint) { - hint.textContent = reason || '请输入访问令牌(LOTTO_API_TOKEN)以继续'; - } - // 自动聚焦输入框 - const input = document.getElementById('loginTokenInput'); - if (input) { - input.value = ''; - setTimeout(() => input.focus(), 50); + hint.textContent = reason || '请输入账号密码以继续'; } + // 清空输入框 + 聚焦用户名 + const userInput = document.getElementById('loginUsernameInput'); + const passInput = document.getElementById('loginPasswordInput'); + if (userInput) { userInput.value = getUsername(); } + if (passInput) { passInput.value = ''; } + setTimeout(() => { + if (userInput && !userInput.value) userInput.focus(); + else if (passInput) passInput.focus(); + }, 50); } function showMainPage() { @@ -1028,14 +1032,17 @@ function showMainPage() { } async function handleLoginSubmit() { - const input = document.getElementById('loginTokenInput'); + const userInput = document.getElementById('loginUsernameInput'); + const passInput = document.getElementById('loginPasswordInput'); const btn = document.getElementById('loginSubmitBtn'); const errEl = document.getElementById('loginError'); - if (!input || !btn) return; - const token = (input.value || '').trim(); - if (!token) { + if (!userInput || !passInput || !btn) return; + + const username = (userInput.value || '').trim(); + const password = passInput.value || ''; + if (!username || !password) { if (errEl) { - errEl.textContent = '令牌不能为空'; + errEl.textContent = '用户名和密码不能为空'; errEl.style.display = 'block'; } return; @@ -1045,19 +1052,40 @@ async function handleLoginSubmit() { const oldText = btn.textContent; btn.textContent = '⏳ 验证中...'; try { - // 试探一下 /api/status 验证 token(BIZ-110:/api/status 不走 require_auth, - // 因此用 /api/records 探测,且带回业务数据) - setToken(token); - await api('/api/records?page=1&page_size=1'); - // 验证成功 + const res = await fetch('/api/auth/login', { + method: 'POST', + credentials: 'same-origin', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ username, password }), + }); + const j = await res.json().catch(() => ({})); + if (!res.ok || !j.success) { + const code = j.code || 'login_failed'; + const msg = j.error || `登录失败(HTTP ${res.status})`; + if (errEl) { + // 锁定提示额外说明 + if (code === 'account_locked') { + errEl.textContent = '⛔ ' + msg; + } else if (code === 'rate_limited') { + errEl.textContent = '⏳ ' + msg; + } else { + errEl.textContent = msg; + } + errEl.style.display = 'block'; + } + btn.disabled = false; + btn.textContent = oldText; + return; + } + // 登录成功 + setUsername(username); showMainPage(); - showToast('✅ 登录成功', 'success'); + showToast('✅ 登录成功(' + (j.data.user.username || username) + ')', 'success'); // 重新加载当前页面数据 if (state.currentPage) switchPage(state.currentPage); } catch (e) { - setToken(''); if (errEl) { - errEl.textContent = '令牌无效:' + (e.message || '验证失败'); + errEl.textContent = '网络错误:' + (e.message || '请检查连接'); errEl.style.display = 'block'; } btn.disabled = false; @@ -1102,11 +1130,13 @@ function switchPage(page) { } // ============================================================ -// Auth (BIZ-110) +// Auth (BIZ-129 方案 C:session cookie + username/password) // ============================================================ -const AUTH_STORAGE_KEY = 'lotto_api_token'; +// 架构决策:HttpOnly cookie 走 session,localStorage 只存 username(用于 UI 提示) +// session 本身由服务器写入 HttpOnly cookie,前端 JS 读不到——完全防 XSS。 +const AUTH_STORAGE_KEY = 'lotto_username'; -function getToken() { +function getUsername() { try { return localStorage.getItem(AUTH_STORAGE_KEY) || ''; } catch (e) { @@ -1114,47 +1144,53 @@ function getToken() { } } -function setToken(t) { +function setUsername(u) { try { - if (t) localStorage.setItem(AUTH_STORAGE_KEY, t); + if (u) localStorage.setItem(AUTH_STORAGE_KEY, u); else localStorage.removeItem(AUTH_STORAGE_KEY); } catch (e) { /* localStorage 不可用(隐私模式)—忽略 */ } } function isLoggedIn() { - return !!getToken(); + // 是否登录仅看 cookie 是否存在;username 仅为 UI 优化 + return document.cookie.split(';').some(c => c.trim().startsWith('lotto_session=')); } -function logout(reason) { - setToken(''); +async function logout(reason) { + // 调后端 logout 吊销 session,然后清本地状态 + try { + await fetch('/api/auth/logout', { method: 'POST', credentials: 'same-origin' }); + } catch (e) { + console.warn('logout 请求失败:', e); + } + setUsername(''); showToast(reason || '已退出登录', 'info'); showLoginPage(); } // ============================================================ -// API Helper (BIZ-110: 注入 Bearer token) +// API Helper (BIZ-129: 不再注入 Bearer,cookie 由浏览器自动带上) // ============================================================ async function api(path, options = {}) { + const opts = { credentials: 'same-origin', ...options }; // 关键:带上 cookie const headers = { 'Content-Type': 'application/json', ...(options.headers || {}) }; - const token = getToken(); - if (token) headers['Authorization'] = 'Bearer ' + token; let res; try { - res = await fetch(path, { ...options, headers }); + res = await fetch(path, { ...opts, headers }); } catch (e) { showToast('网络错误:' + e.message, 'error'); throw e; } - // 401 → 清 token + 跳登录页 + // 401 → 清 username + 跳登录页(cookie 由浏览器自动清除 — 如果服务器返回 Set-Cookie 过期) if (res.status === 401) { let msg = '未授权访问'; try { const j = await res.json(); if (j && j.error) msg = j.error; } catch (_) { /* 忽略 JSON 解析错误 */ } - setToken(''); + setUsername(''); showLoginPage(msg); throw new Error(msg); } @@ -1762,13 +1798,25 @@ async function compareRecord(id) { // ============================================================ // Init // ============================================================ -function bootstrap() { +async function bootstrap() { + // BIZ-129 方案 C:检查 cookie + 调 /api/auth/me 验证 if (!isLoggedIn()) { showLoginPage(); return; } - showMainPage(); - loadStatsOverview(); + try { + const me = await api('/api/auth/me'); + if (me && me.username) { + setUsername(me.username); + showMainPage(); + loadStatsOverview(); + } else { + showLoginPage(); + } + } catch (e) { + // 401 已被 api() 拦截;其他错误 → 跳登录页 + showLoginPage(); + } } bootstrap();