feat: 退出按钮改用SVG图标,密码配置外部化到auth.ini,设置页添加修改密码功能
This commit is contained in:
@@ -44,6 +44,9 @@ logs/
|
|||||||
.env
|
.env
|
||||||
.env.local
|
.env.local
|
||||||
|
|
||||||
|
# 认证配置(含密码)
|
||||||
|
auth.ini
|
||||||
|
|
||||||
# 数据库
|
# 数据库
|
||||||
*.db
|
*.db
|
||||||
*.sqlite
|
*.sqlite
|
||||||
|
|||||||
+50
-5
@@ -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)
|
||||||
|
|||||||
@@ -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,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
@@ -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
@@ -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) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user