From dce07f2f8748ecbc453f6149cc5ff5208f1eb5cb Mon Sep 17 00:00:00 2001 From: Nixevol Date: Thu, 23 Apr 2026 12:03:00 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E9=80=80=E5=87=BA=E6=8C=89=E9=92=AE?= =?UTF-8?q?=E6=94=B9=E7=94=A8SVG=E5=9B=BE=E6=A0=87=EF=BC=8C=E5=AF=86?= =?UTF-8?q?=E7=A0=81=E9=85=8D=E7=BD=AE=E5=A4=96=E9=83=A8=E5=8C=96=E5=88=B0?= =?UTF-8?q?auth.ini=EF=BC=8C=E8=AE=BE=E7=BD=AE=E9=A1=B5=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E4=BF=AE=E6=94=B9=E5=AF=86=E7=A0=81=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 ++ app/main.py | 55 ++++++++++++++++++++++++++++++--- docs/project_context.md | 8 +++++ run.bat | 2 +- static/index.html | 68 ++++++++++++++++++++++++++++++++++++----- static/js/app.js | 33 +++++++++++++++++++- 6 files changed, 155 insertions(+), 14 deletions(-) diff --git a/.gitignore b/.gitignore index 07f06bc..c7dd972 100644 --- a/.gitignore +++ b/.gitignore @@ -44,6 +44,9 @@ logs/ .env .env.local +# 认证配置(含密码) +auth.ini + # 数据库 *.db *.sqlite diff --git a/app/main.py b/app/main.py index 51dbc93..bde6a20 100644 --- a/app/main.py +++ b/app/main.py @@ -24,8 +24,37 @@ import hmac import hashlib import time +import configparser + SECRET_KEY = "CapaReportSecretKey2026" -ADMIN_PASSWORD = "admin" # 默认管理密码 +AUTH_INI_PATH = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "auth.ini") + +def _ensure_auth_ini(): + """确保 auth.ini 存在,不存在则创建默认配置""" + if not os.path.exists(AUTH_INI_PATH): + cfg = configparser.ConfigParser() + cfg["auth"] = {"username": "root", "password": "admin"} + with open(AUTH_INI_PATH, "w", encoding="utf-8") as f: + cfg.write(f) + +def get_auth_config(): + """从 auth.ini 读取认证配置""" + _ensure_auth_ini() + cfg = configparser.ConfigParser() + cfg.read(AUTH_INI_PATH, encoding="utf-8") + return { + "username": cfg.get("auth", "username", fallback="root"), + "password": cfg.get("auth", "password", fallback="admin") + } + +def save_auth_password(new_password: str): + """更新 auth.ini 中的密码""" + _ensure_auth_ini() + cfg = configparser.ConfigParser() + cfg.read(AUTH_INI_PATH, encoding="utf-8") + cfg.set("auth", "password", new_password) + with open(AUTH_INI_PATH, "w", encoding="utf-8") as f: + cfg.write(f) def create_jwt_token(data: dict, expires_in: int = 86400 * 30) -> str: header = base64.urlsafe_b64encode(json.dumps({"alg": "HS256", "typ": "JWT"}).encode()).decode().rstrip("=") @@ -111,12 +140,28 @@ async def jwt_middleware(request: Request, call_next): @app.post("/api/login") async def login(username: str = Body(..., embed=True), password: str = Body(..., embed=True)): - if username != "root" or password != ADMIN_PASSWORD: + auth = get_auth_config() + if username != auth["username"] or password != auth["password"]: return JSONResponse(status_code=401, content={"detail": "账号或密码错误"}) token = create_jwt_token({"user": username}) return {"success": True, "token": token} +@app.post("/api/change-password") +async def change_password( + current_password: str = Body(..., embed=True), + new_password: str = Body(..., embed=True) +): + """修改登录密码""" + auth = get_auth_config() + if current_password != auth["password"]: + return JSONResponse(status_code=400, content={"detail": "当前密码错误"}) + if len(new_password) < 4: + return JSONResponse(status_code=400, content={"detail": "新密码长度不能少于4位"}) + save_auth_password(new_password) + return {"success": True, "message": "密码修改成功"} + + # ==================== 健康检查 ==================== @app.get("/health") @@ -159,7 +204,7 @@ async def health_check(): return { "status": "healthy" if is_healthy else "unhealthy", "timestamp": datetime.now().isoformat(), - "version": "2.0.1", + "version": "2.0.2", "uptime_pid": os.getpid(), "checks": checks } @@ -1193,7 +1238,7 @@ async def get_service_status(): """获取服务运行状态""" return { "status": "running", - "version": "2.0.1", + "version": "2.0.2", "platform": platform.system(), "supervisor": is_supervisor_running(), "pid": os.getpid(), @@ -1336,6 +1381,6 @@ async def save_script_content(content: str = Body(..., embed=True)): if __name__ == "__main__": import uvicorn - print(f"CapacityReport v2.0.1") + print(f"CapacityReport v2.0.2") print(f"配置更新时间: {config.update}") uvicorn.run("app.main:app", host="0.0.0.0", port=9081, reload=False) diff --git a/docs/project_context.md b/docs/project_context.md index 0d81614..a8969f1 100644 --- a/docs/project_context.md +++ b/docs/project_context.md @@ -12,6 +12,14 @@ - **API 401 处理**: `showLoginModal()` 函数保留,内部清除 token 后 `window.location.href = '/'` 跳转到登录页。 - **涉及文件**: `static/login.html`(新增)、`app/main.py`、`static/index.html`、`static/js/app.js`、`static/css/style.css` +### 2026-04-23: 退出 SVG 图标 + INI 密码配置 + 修改密码功能 +- **退出按钮图标**: Unicode `⏻` 在浏览器中不显示,替换为明确的 SVG 退出图标(门+箭头样式)。 +- **密码外部配置**: 账号密码从 `main.py` 硬编码移到 `auth.ini` 文件。使用 `configparser` 读写,每次登录/改密码都实时读取。`auth.ini` 已加入 `.gitignore`。首次运行不存在时自动创建默认配置 `root/admin`。 +- **修改密码功能**: + - 后端 `/api/change-password` 接口,验证当前密码后写入新密码到 `auth.ini`。 + - 前端设置页左侧列新增"修改登录密码"卡片(当前密码 + 新密码 + 确认新密码),修改成功后自动退出重新登录。 +- **涉及文件**: `auth.ini`(新增)、`app/main.py`、`static/index.html`、`static/js/app.js`、`.gitignore` + ### 2026-04-22: 增加 JWT 鉴权和接口安全控制 - **问题背景**: 网管部门通报安全问题,扫描到项目存在暴露的 API 文档(/docs, /redoc, /openapi.json),并且 API 接口没有使用授权控制,要求快速增加鉴权。 - **架构变更**: diff --git a/run.bat b/run.bat index 372aa2a..a8a205e 100644 --- a/run.bat +++ b/run.bat @@ -1,6 +1,6 @@ @echo off chcp 65001 >nul -title CapacityReport v2.0.1 [自动重启模式] +title CapacityReport v2.0.2 [自动重启模式] echo ======================================== echo CapacityReport - 容量报表处理程序 diff --git a/static/index.html b/static/index.html index 381ceed..ebab6bd 100644 --- a/static/index.html +++ b/static/index.html @@ -41,9 +41,9 @@ 系统设置 - @@ -184,7 +190,13 @@ - +
@@ -219,7 +231,13 @@ - +
@@ -319,7 +337,13 @@ - +
@@ -385,7 +409,13 @@ - +
@@ -453,6 +483,30 @@
+ + +
+
+ 修改登录密码 +
+
+
+ + +
+
+ + +
+
+ + +
+
+ +
diff --git a/static/js/app.js b/static/js/app.js index eecd04a..34f5cfc 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -2225,7 +2225,7 @@ function initApp() { // 恢复上次访问的页面 navigation.restorePage(); - console.log('CapacityReport v2.0.1 已加载'); + console.log('CapacityReport v2.0.2 已加载'); // 退出登录按钮事件(事件委托) document.addEventListener('click', (e) => { @@ -2235,6 +2235,37 @@ function initApp() { logout(); } }); + + // 修改密码按钮事件 + const changePwdBtn = $('#changePassword'); + if (changePwdBtn) { + changePwdBtn.addEventListener('click', async () => { + const currentPwd = $('#currentPassword')?.value; + const newPwd = $('#newPassword')?.value; + const confirmPwd = $('#confirmPassword')?.value; + + if (!currentPwd) { showToast('请输入当前密码', 'warning'); return; } + if (!newPwd) { showToast('请输入新密码', 'warning'); return; } + if (newPwd.length < 4) { showToast('新密码长度不能少于4位', 'warning'); return; } + if (newPwd !== confirmPwd) { showToast('两次输入的新密码不一致', 'warning'); return; } + + try { + const res = await api('/change-password', { + method: 'POST', + body: JSON.stringify({ current_password: currentPwd, new_password: newPwd }) + }); + if (res.success) { + showToast('密码修改成功,请重新登录', 'success'); + $('#currentPassword').value = ''; + $('#newPassword').value = ''; + $('#confirmPassword').value = ''; + setTimeout(() => logout(), 1500); + } + } catch (err) { + showToast(err.message || '密码修改失败', 'error'); + } + }); + } // 重启服务按钮事件(使用事件委托,支持所有页面的重启按钮) document.addEventListener('click', async (e) => {