feat: 退出按钮改用SVG图标,密码配置外部化到auth.ini,设置页添加修改密码功能

This commit is contained in:
2026-04-23 12:03:00 +08:00
parent a378943ec0
commit dce07f2f87
6 changed files with 155 additions and 14 deletions
+3
View File
@@ -44,6 +44,9 @@ logs/
.env .env
.env.local .env.local
# 认证配置(含密码)
auth.ini
# 数据库 # 数据库
*.db *.db
*.sqlite *.sqlite
+50 -5
View File
@@ -24,8 +24,37 @@ import hmac
import hashlib import hashlib
import time import time
import configparser
SECRET_KEY = "CapaReportSecretKey2026" 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: 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("=") 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") @app.post("/api/login")
async def login(username: str = Body(..., embed=True), password: str = Body(..., embed=True)): 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": "账号或密码错误"}) return JSONResponse(status_code=401, content={"detail": "账号或密码错误"})
token = create_jwt_token({"user": username}) token = create_jwt_token({"user": username})
return {"success": True, "token": token} 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") @app.get("/health")
@@ -159,7 +204,7 @@ async def health_check():
return { return {
"status": "healthy" if is_healthy else "unhealthy", "status": "healthy" if is_healthy else "unhealthy",
"timestamp": datetime.now().isoformat(), "timestamp": datetime.now().isoformat(),
"version": "2.0.1", "version": "2.0.2",
"uptime_pid": os.getpid(), "uptime_pid": os.getpid(),
"checks": checks "checks": checks
} }
@@ -1193,7 +1238,7 @@ async def get_service_status():
"""获取服务运行状态""" """获取服务运行状态"""
return { return {
"status": "running", "status": "running",
"version": "2.0.1", "version": "2.0.2",
"platform": platform.system(), "platform": platform.system(),
"supervisor": is_supervisor_running(), "supervisor": is_supervisor_running(),
"pid": os.getpid(), "pid": os.getpid(),
@@ -1336,6 +1381,6 @@ async def save_script_content(content: str = Body(..., embed=True)):
if __name__ == "__main__": if __name__ == "__main__":
import uvicorn import uvicorn
print(f"CapacityReport v2.0.1") print(f"CapacityReport v2.0.2")
print(f"配置更新时间: {config.update}") print(f"配置更新时间: {config.update}")
uvicorn.run("app.main:app", host="0.0.0.0", port=9081, reload=False) uvicorn.run("app.main:app", host="0.0.0.0", port=9081, reload=False)
+8
View File
@@ -12,6 +12,14 @@
- **API 401 处理**: `showLoginModal()` 函数保留,内部清除 token 后 `window.location.href = '/'` 跳转到登录页。 - **API 401 处理**: `showLoginModal()` 函数保留,内部清除 token 后 `window.location.href = '/'` 跳转到登录页。
- **涉及文件**: `static/login.html`(新增)、`app/main.py`、`static/index.html`、`static/js/app.js`、`static/css/style.css` - **涉及文件**: `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 鉴权和接口安全控制 ### 2026-04-22: 增加 JWT 鉴权和接口安全控制
- **问题背景**: 网管部门通报安全问题,扫描到项目存在暴露的 API 文档(/docs, /redoc, /openapi.json),并且 API 接口没有使用授权控制,要求快速增加鉴权。 - **问题背景**: 网管部门通报安全问题,扫描到项目存在暴露的 API 文档(/docs, /redoc, /openapi.json),并且 API 接口没有使用授权控制,要求快速增加鉴权。
- **架构变更**: - **架构变更**:
+1 -1
View File
@@ -1,6 +1,6 @@
@echo off @echo off
chcp 65001 >nul chcp 65001 >nul
title CapacityReport v2.0.1 [自动重启模式] title CapacityReport v2.0.2 [自动重启模式]
echo ======================================== echo ========================================
echo CapacityReport - 容量报表处理程序 echo CapacityReport - 容量报表处理程序
+61 -7
View File
@@ -41,9 +41,9 @@
<span>系统设置</span> <span>系统设置</span>
</a> </a>
</nav> </nav>
<div class="sidebar-footer" data-version="v2.0.1"> <div class="sidebar-footer" data-version="v2.0.2">
<div class="sidebar-footer-info"> <div class="sidebar-footer-info">
<span class="version">版本:v2.0.1</span> <span class="version">版本:v2.0.2</span>
<span class="version">Power by:NIXEVOL</span> <span class="version">Power by:NIXEVOL</span>
</div> </div>
</div> </div>
@@ -73,7 +73,13 @@
<line x1="8" y1="4" x2="8" y2="6.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/> <line x1="8" y1="4" x2="8" y2="6.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
</svg> </svg>
</button> </button>
<button class="logout-btn" id="logoutBtn" title="退出登录">⏻</button> <button class="logout-btn" id="logoutBtn" title="退出登录">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<polyline points="16 17 21 12 16 7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<line x1="21" y1="12" x2="9" y2="12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
</div> </div>
</header> </header>
@@ -184,7 +190,13 @@
<line x1="8" y1="4" x2="8" y2="6.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/> <line x1="8" y1="4" x2="8" y2="6.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
</svg> </svg>
</button> </button>
<button class="logout-btn" id="logoutBtn" title="退出登录">⏻</button> <button class="logout-btn" id="logoutBtn" title="退出登录">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<polyline points="16 17 21 12 16 7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<line x1="21" y1="12" x2="9" y2="12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
</div> </div>
</header> </header>
<div class="page-body"> <div class="page-body">
@@ -219,7 +231,13 @@
<line x1="8" y1="4" x2="8" y2="6.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/> <line x1="8" y1="4" x2="8" y2="6.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
</svg> </svg>
</button> </button>
<button class="logout-btn" id="logoutBtn" title="退出登录">⏻</button> <button class="logout-btn" id="logoutBtn" title="退出登录">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<polyline points="16 17 21 12 16 7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<line x1="21" y1="12" x2="9" y2="12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
</div> </div>
</header> </header>
<div class="page-body" style="display: flex; flex-direction: column; height: calc(100% - 64px);"> <div class="page-body" style="display: flex; flex-direction: column; height: calc(100% - 64px);">
@@ -319,7 +337,13 @@
<line x1="8" y1="4" x2="8" y2="6.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/> <line x1="8" y1="4" x2="8" y2="6.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
</svg> </svg>
</button> </button>
<button class="logout-btn" id="logoutBtn" title="退出登录">⏻</button> <button class="logout-btn" id="logoutBtn" title="退出登录">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<polyline points="16 17 21 12 16 7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<line x1="21" y1="12" x2="9" y2="12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
</div> </div>
</header> </header>
<div class="page-body script-page-body"> <div class="page-body script-page-body">
@@ -385,7 +409,13 @@
<line x1="8" y1="4" x2="8" y2="6.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/> <line x1="8" y1="4" x2="8" y2="6.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
</svg> </svg>
</button> </button>
<button class="logout-btn" id="logoutBtn" title="退出登录">⏻</button> <button class="logout-btn" id="logoutBtn" title="退出登录">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<polyline points="16 17 21 12 16 7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<line x1="21" y1="12" x2="9" y2="12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
</div> </div>
</header> </header>
<div class="page-body settings-page-body"> <div class="page-body settings-page-body">
@@ -453,6 +483,30 @@
<button class="btn btn-primary" id="saveSheetFilter">保存规则</button> <button class="btn btn-primary" id="saveSheetFilter">保存规则</button>
</div> </div>
</div> </div>
<!-- 修改密码 -->
<div class="card settings-card">
<div class="card-header">
<span class="card-title">修改登录密码</span>
</div>
<div class="card-body">
<div class="form-group">
<label>当前密码</label>
<input type="password" id="currentPassword" class="form-input" placeholder="输入当前密码">
</div>
<div class="form-group" style="margin-top: 12px;">
<label>新密码</label>
<input type="password" id="newPassword" class="form-input" placeholder="输入新密码">
</div>
<div class="form-group" style="margin-top: 12px;">
<label>确认新密码</label>
<input type="password" id="confirmPassword" class="form-input" placeholder="再次输入新密码">
</div>
</div>
<div class="card-footer">
<button class="btn btn-primary" id="changePassword">修改密码</button>
</div>
</div>
</div> </div>
<!-- 右侧列 - 字段映射 --> <!-- 右侧列 - 字段映射 -->
+32 -1
View File
@@ -2225,7 +2225,7 @@ function initApp() {
// 恢复上次访问的页面 // 恢复上次访问的页面
navigation.restorePage(); navigation.restorePage();
console.log('CapacityReport v2.0.1 已加载'); console.log('CapacityReport v2.0.2 已加载');
// 退出登录按钮事件(事件委托) // 退出登录按钮事件(事件委托)
document.addEventListener('click', (e) => { document.addEventListener('click', (e) => {
@@ -2235,6 +2235,37 @@ function initApp() {
logout(); 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) => { document.addEventListener('click', async (e) => {