From dcc733938430fff055bc670036d7ba702ae6e597 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=90=E8=81=AA?= Date: Wed, 2 Sep 2026 16:45:43 +0800 Subject: [PATCH] =?UTF-8?q?feat(BIZ-110):=20index.html=20=E5=89=8D?= =?UTF-8?q?=E7=AB=AF=E9=89=B4=E6=9D=83=E9=80=82=E9=85=8D=20-=20localStorag?= =?UTF-8?q?e=20+=20login=20=E9=A1=B5=20+=20Bearer=20=E6=B3=A8=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 方案 A 落地(严维序 COO 09-01 14:46 GMT+8 确认): - 新增登录浮层(#/login 概念,hash 路由由 bootstrap() 管控) - localStorage.lotto_api_token 存/取/清 token - api() 函数自动注入 'Authorization: Bearer ' - 401 → 清 token + 跳登录页 - Header 右侧 '退出' 按钮 - bootstrap() 启动入口:无 token 跳登录,有 token 进主页 测试:tests/mock_auth_server.py + jsdom 21/21 全过。 闭环:前端适配完成,待严维序撤 AUTH_BYPASS_PATHS 白名单。 —— 徐聪 (costcodev) | 2026-09-02 16:35 GMT+8 Co-authored-by: multica-agent --- index.html | 249 ++++++++++++++++++++++++++++++++++++-- tests/mock_auth_server.py | 51 ++++++++ 2 files changed, 290 insertions(+), 10 deletions(-) create mode 100644 tests/mock_auth_server.py diff --git a/index.html b/index.html index 3e7fdc3..67eb9f7 100644 --- a/index.html +++ b/index.html @@ -51,6 +51,83 @@ } .header h1 { font-size: 22px; font-weight: 700; letter-spacing: 1px; } .header p { font-size: 13px; opacity: 0.85; margin-top: 4px; } + .header .logout-btn { + position: absolute; + right: 18px; + top: 50%; + transform: translateY(-50%); + background: rgba(255,255,255,0.18); + color: white; + border: 1px solid rgba(255,255,255,0.35); + padding: 6px 14px; + border-radius: 6px; + font-size: 13px; + cursor: pointer; + transition: all 0.2s; + } + .header .logout-btn:hover { background: rgba(255,255,255,0.32); } + + /* ============================================================ + Login Overlay (BIZ-110) + ============================================================ */ + .login-overlay { + display: none; + position: fixed; + inset: 0; + background: linear-gradient(135deg, #e74c3c 0%, #c0392b 50%, #8e44ad 100%); + z-index: 9999; + align-items: center; + justify-content: center; + padding: 20px; + } + .login-overlay.show { display: flex; } + .login-card { + background: white; + border-radius: 16px; + padding: 36px 32px; + width: 100%; + max-width: 400px; + box-shadow: 0 20px 60px rgba(0,0,0,0.3); + text-align: center; + } + .login-title { font-size: 22px; font-weight: 700; color: var(--text); margin-bottom: 6px; } + .login-subtitle { font-size: 13px; color: var(--text-light); margin-bottom: 22px; } + .login-hint { font-size: 13px; color: var(--text-light); margin-bottom: 12px; } + .login-input { + width: 100%; + padding: 12px 14px; + border: 1px solid var(--border); + border-radius: 8px; + font-size: 15px; + outline: none; + transition: border-color 0.2s; + margin-bottom: 12px; + } + .login-input:focus { border-color: var(--primary); box-shadow: 0 0 0 3px rgba(231,76,60,0.1); } + .login-btn { + width: 100%; + padding: 12px 24px; + background: linear-gradient(135deg, #e74c3c, #c0392b); + color: white; + border: none; + border-radius: 8px; + font-size: 15px; + font-weight: 600; + cursor: pointer; + transition: all 0.2s; + } + .login-btn:hover { transform: translateY(-1px); box-shadow: 0 4px 12px rgba(231,76,60,0.3); } + .login-btn:disabled { opacity: 0.6; cursor: not-allowed; transform: none; } + .login-error { + margin-top: 12px; + padding: 10px 12px; + background: #fdecea; + color: #c0392b; + border-radius: 6px; + font-size: 13px; + text-align: left; + } + .login-foot { font-size: 12px; color: var(--text-light); margin-top: 16px; } /* ============================================================ Navigation Tabs @@ -728,6 +805,23 @@ + + + +
+ @@ -737,6 +831,7 @@ Toast Header ============================================================ -->
+

🎯 双色球号码生成系统

基于历史数据分析 · 支持 PC 端和移动端

@@ -902,6 +997,74 @@ const state = { searchTimer: null, }; +// ============================================================ +// Login Page (BIZ-110) +// ============================================================ +function showLoginPage(reason) { + const overlay = document.getElementById('loginOverlay'); + if (!overlay) return; + overlay.classList.add('show'); + // 隐藏主内容 + const main = document.getElementById('mainApp'); + if (main) main.style.display = 'none'; + // 提示信息 + 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); + } +} + +function showMainPage() { + const overlay = document.getElementById('loginOverlay'); + if (overlay) overlay.classList.remove('show'); + const main = document.getElementById('mainApp'); + if (main) main.style.display = ''; +} + +async function handleLoginSubmit() { + const input = document.getElementById('loginTokenInput'); + const btn = document.getElementById('loginSubmitBtn'); + const errEl = document.getElementById('loginError'); + if (!input || !btn) return; + const token = (input.value || '').trim(); + if (!token) { + if (errEl) { + errEl.textContent = '令牌不能为空'; + errEl.style.display = 'block'; + } + return; + } + if (errEl) errEl.style.display = 'none'; + btn.disabled = true; + 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'); + // 验证成功 + showMainPage(); + showToast('✅ 登录成功', 'success'); + // 重新加载当前页面数据 + if (state.currentPage) switchPage(state.currentPage); + } catch (e) { + setToken(''); + if (errEl) { + errEl.textContent = '令牌无效:' + (e.message || '验证失败'); + errEl.style.display = 'block'; + } + btn.disabled = false; + btn.textContent = oldText; + } +} + // ============================================================ // Toast // ============================================================ @@ -939,21 +1102,76 @@ function switchPage(page) { } // ============================================================ -// API Helper +// Auth (BIZ-110) +// ============================================================ +const AUTH_STORAGE_KEY = 'lotto_api_token'; + +function getToken() { + try { + return localStorage.getItem(AUTH_STORAGE_KEY) || ''; + } catch (e) { + return ''; + } +} + +function setToken(t) { + try { + if (t) localStorage.setItem(AUTH_STORAGE_KEY, t); + else localStorage.removeItem(AUTH_STORAGE_KEY); + } catch (e) { /* localStorage 不可用(隐私模式)—忽略 */ } +} + +function isLoggedIn() { + return !!getToken(); +} + +function logout(reason) { + setToken(''); + showToast(reason || '已退出登录', 'info'); + showLoginPage(); +} + +// ============================================================ +// API Helper (BIZ-110: 注入 Bearer token) // ============================================================ async function api(path, options = {}) { + const headers = { 'Content-Type': 'application/json', ...(options.headers || {}) }; + const token = getToken(); + if (token) headers['Authorization'] = 'Bearer ' + token; + + let res; try { - const res = await fetch(path, { - headers: { 'Content-Type': 'application/json', ...options.headers }, - ...options - }); - const data = await res.json(); - if (!data.success) throw new Error(data.error || '请求失败'); - return data.data; + res = await fetch(path, { ...options, headers }); } catch (e) { - showToast(e.message, 'error'); + showToast('网络错误:' + e.message, 'error'); throw e; } + + // 401 → 清 token + 跳登录页 + if (res.status === 401) { + let msg = '未授权访问'; + try { + const j = await res.json(); + if (j && j.error) msg = j.error; + } catch (_) { /* 忽略 JSON 解析错误 */ } + setToken(''); + showLoginPage(msg); + throw new Error(msg); + } + + let data; + try { + data = await res.json(); + } catch (e) { + showToast('响应解析失败', 'error'); + throw e; + } + + if (!data.success) { + showToast(data.error || '请求失败', 'error'); + throw new Error(data.error || '请求失败'); + } + return data.data; } // ============================================================ @@ -1544,7 +1762,18 @@ async function compareRecord(id) { // ============================================================ // Init // ============================================================ -loadStatsOverview(); +function bootstrap() { + if (!isLoggedIn()) { + showLoginPage(); + return; + } + showMainPage(); + loadStatsOverview(); +} + +bootstrap(); + +
diff --git a/tests/mock_auth_server.py b/tests/mock_auth_server.py new file mode 100644 index 0000000..8f84598 --- /dev/null +++ b/tests/mock_auth_server.py @@ -0,0 +1,51 @@ +""" +BIZ-110 端到端 mock:模拟后端鉴权 + 前端 token 注入验证。 +- GET /api/status: 永远 200,无 token 校验(status 探测) +- 其他 /api/* : 必须 Authorization: Bearer ,token = 'test-token-ok' 通过 +""" +import http.server, json, sys + +VALID_TOKEN = 'test-token-ok' + +class H(http.server.BaseHTTPRequestHandler): + def log_message(self, fmt, *a): pass # quiet + + def _send(self, code, payload): + body = json.dumps(payload).encode() + self.send_response(code) + self.send_header('Content-Type', 'application/json') + self.send_header('Content-Length', str(len(body))) + self.end_headers() + self.wfile.write(body) + + def _check_auth(self): + h = self.headers.get('Authorization', '') + if not h.startswith('Bearer '): + return False + return h[7:] == VALID_TOKEN + + def do_GET(self): + if self.path.startswith('/api/status'): + return self._send(200, {'success': True, 'data': {'ok': True}}) + if self.path.startswith('/api/records'): + if not self._check_auth(): + return self._send(401, {'success': False, 'error': '未授权访问'}) + return self._send(200, {'success': True, 'data': {'records': [], 'total': 0, 'page': 1}}) + return self._send(404, {'success': False, 'error': 'not found'}) + + def do_POST(self): + if self.path.startswith('/api/records') and 'fake-id' in self.path: + return self._send(404, {'success': False, 'error': 'not found'}) + return self._send(404, {'success': False, 'error': 'not found'}) + + def do_DELETE(self): + if not self._check_auth(): + return self._send(401, {'success': False, 'error': '未授权访问'}) + return self._send(200, {'success': True, 'data': {}}) + + +if __name__ == '__main__': + port = int(sys.argv[1]) if len(sys.argv) > 1 else 18085 + srv = http.server.HTTPServer(('127.0.0.1', port), H) + print(f'mock listening on {port}') + srv.serve_forever()