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)
+
+
-
+
@@ -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();