security: harden admin password change and production session headers

This commit is contained in:
2026-02-07 21:37:55 +08:00
parent 7997a97a9a
commit 08864e51ba
26 changed files with 159 additions and 59 deletions
+2 -1
View File
@@ -13,7 +13,8 @@ FLASK_DEBUG=false
# Session配置 # Session配置
SESSION_LIFETIME_HOURS=24 SESSION_LIFETIME_HOURS=24
SESSION_COOKIE_SECURE=false # 使用HTTPS时设为true SESSION_COOKIE_SECURE=true # 生产环境HTTPS必须为true,本地HTTP调试可临时设为false
HTTPS_ENABLED=true
# ==================== 数据库配置 ==================== # ==================== 数据库配置 ====================
DB_FILE=data/app_data.db DB_FILE=data/app_data.db
+7 -3
View File
@@ -5,8 +5,13 @@ export async function updateAdminUsername(newUsername) {
return data return data
} }
export async function updateAdminPassword(newPassword) { export async function updateAdminPassword(payload = {}) {
const { data } = await api.put('/admin/password', { new_password: newPassword }) const currentPassword = String(payload.currentPassword || '')
const newPassword = String(payload.newPassword || '')
const { data } = await api.put('/admin/password', {
current_password: currentPassword,
new_password: newPassword,
})
return data return data
} }
@@ -14,4 +19,3 @@ export async function logout() {
const { data } = await api.post('/logout') const { data } = await api.post('/logout')
return data return data
} }
+39 -1
View File
@@ -5,7 +5,9 @@ import { ElMessage, ElMessageBox } from 'element-plus'
import { logout, updateAdminPassword, updateAdminUsername } from '../api/admin' import { logout, updateAdminPassword, updateAdminUsername } from '../api/admin'
const username = ref('') const username = ref('')
const currentPassword = ref('')
const password = ref('') const password = ref('')
const confirmPassword = ref('')
const submitting = ref(false) const submitting = ref(false)
function validateStrongPassword(value) { function validateStrongPassword(value) {
@@ -57,17 +59,31 @@ async function saveUsername() {
} }
async function savePassword() { async function savePassword() {
const currentValue = currentPassword.value
const value = password.value const value = password.value
const confirmValue = confirmPassword.value
if (!currentValue) {
ElMessage.error('请输入当前密码')
return
}
if (!value) { if (!value) {
ElMessage.error('请输入新密码') ElMessage.error('请输入新密码')
return return
} }
const check = validateStrongPassword(value) const check = validateStrongPassword(value)
if (!check.ok) { if (!check.ok) {
ElMessage.error(check.message) ElMessage.error(check.message)
return return
} }
if (value !== confirmValue) {
ElMessage.error('两次输入的新密码不一致')
return
}
try { try {
await ElMessageBox.confirm('确定修改管理员密码吗?修改后需要重新登录。', '修改密码', { await ElMessageBox.confirm('确定修改管理员密码吗?修改后需要重新登录。', '修改密码', {
confirmButtonText: '确认修改', confirmButtonText: '确认修改',
@@ -80,9 +96,11 @@ async function savePassword() {
submitting.value = true submitting.value = true
try { try {
await updateAdminPassword(value) await updateAdminPassword({ currentPassword: currentValue, newPassword: value })
ElMessage.success('密码修改成功,请重新登录') ElMessage.success('密码修改成功,请重新登录')
currentPassword.value = ''
password.value = '' password.value = ''
confirmPassword.value = ''
setTimeout(relogin, 1200) setTimeout(relogin, 1200)
} catch { } catch {
// handled by interceptor // handled by interceptor
@@ -112,6 +130,16 @@ async function savePassword() {
<el-card shadow="never" :body-style="{ padding: '16px' }" class="card"> <el-card shadow="never" :body-style="{ padding: '16px' }" class="card">
<h3 class="section-title">修改管理员密码</h3> <h3 class="section-title">修改管理员密码</h3>
<el-form label-width="120px"> <el-form label-width="120px">
<el-form-item label="当前密码">
<el-input
v-model="currentPassword"
type="password"
show-password
placeholder="输入当前密码"
:disabled="submitting"
/>
</el-form-item>
<el-form-item label="新密码"> <el-form-item label="新密码">
<el-input <el-input
v-model="password" v-model="password"
@@ -121,6 +149,16 @@ async function savePassword() {
:disabled="submitting" :disabled="submitting"
/> />
</el-form-item> </el-form-item>
<el-form-item label="确认新密码">
<el-input
v-model="confirmPassword"
type="password"
show-password
placeholder="再次输入新密码"
:disabled="submitting"
/>
</el-form-item>
</el-form> </el-form>
<el-button type="primary" :loading="submitting" @click="savePassword">保存密码</el-button> <el-button type="primary" :loading="submitting" @click="savePassword">保存密码</el-button>
<div class="help">建议使用更强密码至少8位且包含字母与数字</div> <div class="help">建议使用更强密码至少8位且包含字母与数字</div>
+36
View File
@@ -135,6 +135,33 @@ def _is_api_or_health_path(path: str) -> bool:
return raw.startswith("/api/") or raw.startswith("/yuyx/api/") or raw == "/health" return raw.startswith("/api/") or raw.startswith("/yuyx/api/") or raw == "/health"
def _request_uses_https() -> bool:
try:
if bool(request.is_secure):
return True
except Exception:
pass
try:
forwarded_proto = str(request.headers.get("X-Forwarded-Proto", "") or "").split(",", 1)[0].strip().lower()
if forwarded_proto == "https":
return True
except Exception:
pass
return False
_SECURITY_RESPONSE_HEADERS = {
"X-Content-Type-Options": "nosniff",
"X-Frame-Options": "SAMEORIGIN",
"Referrer-Policy": "strict-origin-when-cross-origin",
"Permissions-Policy": "camera=(), microphone=(), geolocation=(), payment=()",
}
_SECURITY_CSP_HEADER = str(os.environ.get("SECURITY_CONTENT_SECURITY_POLICY", "") or "").strip()
_HASHED_STATIC_ASSET_RE = re.compile(r".*-[a-z0-9_-]{8,}\.(?:js|css|woff2?|ttf|svg|png|jpe?g|webp)$", re.IGNORECASE) _HASHED_STATIC_ASSET_RE = re.compile(r".*-[a-z0-9_-]{8,}\.(?:js|css|woff2?|ttf|svg|png|jpe?g|webp)$", re.IGNORECASE)
@@ -238,6 +265,15 @@ def ensure_csrf_cookie(response):
samesite=config.SESSION_COOKIE_SAMESITE, samesite=config.SESSION_COOKIE_SAMESITE,
) )
for header_name, header_value in _SECURITY_RESPONSE_HEADERS.items():
response.headers.setdefault(header_name, header_value)
if _request_uses_https():
response.headers.setdefault("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
if _SECURITY_CSP_HEADER:
response.headers.setdefault("Content-Security-Policy", _SECURITY_CSP_HEADER)
_record_request_metric_after_response(response) _record_request_metric_after_response(response)
return response return response
+2 -1
View File
@@ -48,7 +48,8 @@ services:
# 加密密钥配置(重要!防止容器重建时丢失密钥) # 加密密钥配置(重要!防止容器重建时丢失密钥)
- ENCRYPTION_KEY_RAW=${ENCRYPTION_KEY_RAW} - ENCRYPTION_KEY_RAW=${ENCRYPTION_KEY_RAW}
- SESSION_LIFETIME_HOURS=24 - SESSION_LIFETIME_HOURS=24
- SESSION_COOKIE_SECURE=false - SESSION_COOKIE_SECURE=true
- HTTPS_ENABLED=true
- MAX_CAPTCHA_ATTEMPTS=5 - MAX_CAPTCHA_ATTEMPTS=5
- MAX_IP_ATTEMPTS_PER_HOUR=10 - MAX_IP_ATTEMPTS_PER_HOUR=10
# 日志配置 # 日志配置
+22 -2
View File
@@ -14,15 +14,35 @@ from routes.decorators import admin_required
@admin_api_bp.route("/admin/password", methods=["PUT"]) @admin_api_bp.route("/admin/password", methods=["PUT"])
@admin_required @admin_required
def update_admin_password(): def update_admin_password():
"""修改管理员密码""" """修改管理员密码(要求提供当前密码并校验新密码强度)"""
data = request.json or {} data = request.json or {}
current_password = (data.get("current_password") or "").strip()
new_password = (data.get("new_password") or "").strip() new_password = (data.get("new_password") or "").strip()
if not current_password:
return jsonify({"error": "当前密码不能为空"}), 400
if not new_password: if not new_password:
return jsonify({"error": "密码不能为空"}), 400 return jsonify({"error": "密码不能为空"}), 400
if current_password == new_password:
return jsonify({"error": "新密码不能与当前密码相同"}), 400
is_valid, error_msg = validate_password(new_password)
if not is_valid:
return jsonify({"error": error_msg}), 400
username = session.get("admin_username") username = session.get("admin_username")
if not username:
return jsonify({"error": "未登录"}), 401
admin = database.verify_admin(username, current_password)
if not admin:
return jsonify({"error": "当前密码错误"}), 401
if database.update_admin_password(username, new_password): if database.update_admin_password(username, new_password):
session["admin_reauth_until"] = 0
session.modified = True
return jsonify({"success": True}) return jsonify({"success": True})
return jsonify({"error": "修改失败"}), 400 return jsonify({"error": "修改失败"}), 400
+33 -33
View File
@@ -1,6 +1,6 @@
{ {
"_MetricGrid-Bj2rgY20.js": { "_MetricGrid-Dsqo4YZI.js": {
"file": "assets/MetricGrid-Bj2rgY20.js", "file": "assets/MetricGrid-Dsqo4YZI.js",
"name": "MetricGrid", "name": "MetricGrid",
"imports": [ "imports": [
"index.html", "index.html",
@@ -14,29 +14,29 @@
"file": "assets/MetricGrid-yP_dkP6X.css", "file": "assets/MetricGrid-yP_dkP6X.css",
"src": "_MetricGrid-yP_dkP6X.css" "src": "_MetricGrid-yP_dkP6X.css"
}, },
"_email-UyqcDGmM.js": { "_email--WygXDwI.js": {
"file": "assets/email-UyqcDGmM.js", "file": "assets/email--WygXDwI.js",
"name": "email", "name": "email",
"imports": [ "imports": [
"index.html" "index.html"
] ]
}, },
"_system-CCwKPotj.js": { "_system-CAzjuaad.js": {
"file": "assets/system-CCwKPotj.js", "file": "assets/system-CAzjuaad.js",
"name": "system", "name": "system",
"imports": [ "imports": [
"index.html" "index.html"
] ]
}, },
"_tasks-BpEmoe5G.js": { "_tasks-OWsi7T-E.js": {
"file": "assets/tasks-BpEmoe5G.js", "file": "assets/tasks-OWsi7T-E.js",
"name": "tasks", "name": "tasks",
"imports": [ "imports": [
"index.html" "index.html"
] ]
}, },
"_users-DXFAixXH.js": { "_users-BZkLUJZL.js": {
"file": "assets/users-DXFAixXH.js", "file": "assets/users-BZkLUJZL.js",
"name": "users", "name": "users",
"imports": [ "imports": [
"index.html" "index.html"
@@ -73,7 +73,7 @@
"name": "vendor-vue" "name": "vendor-vue"
}, },
"index.html": { "index.html": {
"file": "assets/index-COtE2fCT.js", "file": "assets/index-BsqM_wut.js",
"name": "index", "name": "index",
"src": "index.html", "src": "index.html",
"isEntry": true, "isEntry": true,
@@ -99,7 +99,7 @@
] ]
}, },
"src/pages/AnnouncementsPage.vue": { "src/pages/AnnouncementsPage.vue": {
"file": "assets/AnnouncementsPage-CB2ecIAp.js", "file": "assets/AnnouncementsPage-PdPHO5Q2.js",
"name": "AnnouncementsPage", "name": "AnnouncementsPage",
"src": "src/pages/AnnouncementsPage.vue", "src": "src/pages/AnnouncementsPage.vue",
"isDynamicEntry": true, "isDynamicEntry": true,
@@ -115,14 +115,14 @@
] ]
}, },
"src/pages/EmailPage.vue": { "src/pages/EmailPage.vue": {
"file": "assets/EmailPage-DMQiuF7T.js", "file": "assets/EmailPage-yqRvXEJ2.js",
"name": "EmailPage", "name": "EmailPage",
"src": "src/pages/EmailPage.vue", "src": "src/pages/EmailPage.vue",
"isDynamicEntry": true, "isDynamicEntry": true,
"imports": [ "imports": [
"_email-UyqcDGmM.js", "_email--WygXDwI.js",
"index.html", "index.html",
"_MetricGrid-Bj2rgY20.js", "_MetricGrid-Dsqo4YZI.js",
"_vendor-element-B5S5pUKo.js", "_vendor-element-B5S5pUKo.js",
"_vendor-vue-CVxSw_oJ.js", "_vendor-vue-CVxSw_oJ.js",
"_vendor-axios-B9ygI19o.js", "_vendor-axios-B9ygI19o.js",
@@ -133,13 +133,13 @@
] ]
}, },
"src/pages/FeedbacksPage.vue": { "src/pages/FeedbacksPage.vue": {
"file": "assets/FeedbacksPage-DPSbObwj.js", "file": "assets/FeedbacksPage-9Z4ULgo9.js",
"name": "FeedbacksPage", "name": "FeedbacksPage",
"src": "src/pages/FeedbacksPage.vue", "src": "src/pages/FeedbacksPage.vue",
"isDynamicEntry": true, "isDynamicEntry": true,
"imports": [ "imports": [
"index.html", "index.html",
"_MetricGrid-Bj2rgY20.js", "_MetricGrid-Dsqo4YZI.js",
"_vendor-element-B5S5pUKo.js", "_vendor-element-B5S5pUKo.js",
"_vendor-vue-CVxSw_oJ.js", "_vendor-vue-CVxSw_oJ.js",
"_vendor-axios-B9ygI19o.js", "_vendor-axios-B9ygI19o.js",
@@ -150,13 +150,13 @@
] ]
}, },
"src/pages/LogsPage.vue": { "src/pages/LogsPage.vue": {
"file": "assets/LogsPage-C3EIiKOD.js", "file": "assets/LogsPage-MDq3eoIe.js",
"name": "LogsPage", "name": "LogsPage",
"src": "src/pages/LogsPage.vue", "src": "src/pages/LogsPage.vue",
"isDynamicEntry": true, "isDynamicEntry": true,
"imports": [ "imports": [
"_users-DXFAixXH.js", "_users-BZkLUJZL.js",
"_tasks-BpEmoe5G.js", "_tasks-OWsi7T-E.js",
"index.html", "index.html",
"_vendor-element-B5S5pUKo.js", "_vendor-element-B5S5pUKo.js",
"_vendor-vue-CVxSw_oJ.js", "_vendor-vue-CVxSw_oJ.js",
@@ -168,17 +168,17 @@
] ]
}, },
"src/pages/ReportPage.vue": { "src/pages/ReportPage.vue": {
"file": "assets/ReportPage-B5zHPJFA.js", "file": "assets/ReportPage-ycVtg2rZ.js",
"name": "ReportPage", "name": "ReportPage",
"src": "src/pages/ReportPage.vue", "src": "src/pages/ReportPage.vue",
"isDynamicEntry": true, "isDynamicEntry": true,
"imports": [ "imports": [
"_vendor-element-B5S5pUKo.js", "_vendor-element-B5S5pUKo.js",
"index.html", "index.html",
"_email-UyqcDGmM.js", "_email--WygXDwI.js",
"_tasks-BpEmoe5G.js", "_tasks-OWsi7T-E.js",
"_system-CCwKPotj.js", "_system-CAzjuaad.js",
"_MetricGrid-Bj2rgY20.js", "_MetricGrid-Dsqo4YZI.js",
"_vendor-vue-CVxSw_oJ.js", "_vendor-vue-CVxSw_oJ.js",
"_vendor-misc-BeoNyvBp.js", "_vendor-misc-BeoNyvBp.js",
"_vendor-axios-B9ygI19o.js" "_vendor-axios-B9ygI19o.js"
@@ -188,13 +188,13 @@
] ]
}, },
"src/pages/SecurityPage.vue": { "src/pages/SecurityPage.vue": {
"file": "assets/SecurityPage-ClrVJm-9.js", "file": "assets/SecurityPage-CXcU2SbL.js",
"name": "SecurityPage", "name": "SecurityPage",
"src": "src/pages/SecurityPage.vue", "src": "src/pages/SecurityPage.vue",
"isDynamicEntry": true, "isDynamicEntry": true,
"imports": [ "imports": [
"index.html", "index.html",
"_MetricGrid-Bj2rgY20.js", "_MetricGrid-Dsqo4YZI.js",
"_vendor-element-B5S5pUKo.js", "_vendor-element-B5S5pUKo.js",
"_vendor-vue-CVxSw_oJ.js", "_vendor-vue-CVxSw_oJ.js",
"_vendor-axios-B9ygI19o.js", "_vendor-axios-B9ygI19o.js",
@@ -205,7 +205,7 @@
] ]
}, },
"src/pages/SettingsPage.vue": { "src/pages/SettingsPage.vue": {
"file": "assets/SettingsPage-COD6KO3P.js", "file": "assets/SettingsPage-CUZAbAFF.js",
"name": "SettingsPage", "name": "SettingsPage",
"src": "src/pages/SettingsPage.vue", "src": "src/pages/SettingsPage.vue",
"isDynamicEntry": true, "isDynamicEntry": true,
@@ -217,16 +217,16 @@
"_vendor-misc-BeoNyvBp.js" "_vendor-misc-BeoNyvBp.js"
], ],
"css": [ "css": [
"assets/SettingsPage-DaB8PeRL.css" "assets/SettingsPage-NWcEVLn7.css"
] ]
}, },
"src/pages/SystemPage.vue": { "src/pages/SystemPage.vue": {
"file": "assets/SystemPage-CkuZRgDH.js", "file": "assets/SystemPage-B2BrKkTP.js",
"name": "SystemPage", "name": "SystemPage",
"src": "src/pages/SystemPage.vue", "src": "src/pages/SystemPage.vue",
"isDynamicEntry": true, "isDynamicEntry": true,
"imports": [ "imports": [
"_system-CCwKPotj.js", "_system-CAzjuaad.js",
"index.html", "index.html",
"_vendor-element-B5S5pUKo.js", "_vendor-element-B5S5pUKo.js",
"_vendor-vue-CVxSw_oJ.js", "_vendor-vue-CVxSw_oJ.js",
@@ -238,12 +238,12 @@
] ]
}, },
"src/pages/UsersPage.vue": { "src/pages/UsersPage.vue": {
"file": "assets/UsersPage-CwNX_aN-.js", "file": "assets/UsersPage-yptpHEoN.js",
"name": "UsersPage", "name": "UsersPage",
"src": "src/pages/UsersPage.vue", "src": "src/pages/UsersPage.vue",
"isDynamicEntry": true, "isDynamicEntry": true,
"imports": [ "imports": [
"_users-DXFAixXH.js", "_users-BZkLUJZL.js",
"index.html", "index.html",
"_vendor-element-B5S5pUKo.js", "_vendor-element-B5S5pUKo.js",
"_vendor-vue-CVxSw_oJ.js", "_vendor-vue-CVxSw_oJ.js",
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
import{_}from"./index-COtE2fCT.js";import{aj as c,n as s,q as t,K as r,a3 as u,y as p,t as o,G as l,L as y,E as h,D as i,H as v,J as n,I as k,x as f}from"./vendor-vue-CVxSw_oJ.js";const b={class:"metric-top"},x={key:0,class:"metric-icon"},g={class:"metric-label"},B={class:"metric-value"},C={key:0,class:"metric-hint app-muted"},N={__name:"MetricGrid",props:{items:{type:Array,default:()=>[]},loading:{type:Boolean,default:!1},minWidth:{type:Number,default:180}},setup(a){return(V,D)=>{const d=c("el-icon"),m=c("el-skeleton");return t(),s("div",{class:"metric-grid",style:f({"--metric-min":`${a.minWidth}px`})},[(t(!0),s(r,null,u(a.items,e=>(t(),s("div",{key:e?.key||e?.label,class:p(["metric-card",`metric-tone--${e?.tone||"blue"}`])},[o("div",b,[e?.icon?(t(),s("div",x,[y(d,null,{default:h(()=>[(t(),i(v(e.icon)))]),_:2},1024)])):l("",!0),o("div",g,n(e?.label||"-"),1)]),o("div",B,[a.loading?(t(),i(m,{key:0,rows:1,animated:""})):(t(),s(r,{key:1},[k(n(e?.value??0),1)],64))]),e?.hint||e?.sub?(t(),s("div",C,n(e?.hint||e?.sub),1)):l("",!0)],2))),128))],4)}}},w=_(N,[["__scopeId","data-v-00e217d4"]]);export{w as M}; import{_}from"./index-BsqM_wut.js";import{aj as c,n as s,q as t,K as r,a3 as u,y as p,t as o,G as l,L as y,E as h,D as i,H as v,J as n,I as k,x as f}from"./vendor-vue-CVxSw_oJ.js";const b={class:"metric-top"},x={key:0,class:"metric-icon"},g={class:"metric-label"},B={class:"metric-value"},C={key:0,class:"metric-hint app-muted"},N={__name:"MetricGrid",props:{items:{type:Array,default:()=>[]},loading:{type:Boolean,default:!1},minWidth:{type:Number,default:180}},setup(a){return(V,D)=>{const d=c("el-icon"),m=c("el-skeleton");return t(),s("div",{class:"metric-grid",style:f({"--metric-min":`${a.minWidth}px`})},[(t(!0),s(r,null,u(a.items,e=>(t(),s("div",{key:e?.key||e?.label,class:p(["metric-card",`metric-tone--${e?.tone||"blue"}`])},[o("div",b,[e?.icon?(t(),s("div",x,[y(d,null,{default:h(()=>[(t(),i(v(e.icon)))]),_:2},1024)])):l("",!0),o("div",g,n(e?.label||"-"),1)]),o("div",B,[a.loading?(t(),i(m,{key:0,rows:1,animated:""})):(t(),s(r,{key:1},[k(n(e?.value??0),1)],64))]),e?.hint||e?.sub?(t(),s("div",C,n(e?.hint||e?.sub),1)):l("",!0)],2))),128))],4)}}},w=_(N,[["__scopeId","data-v-00e217d4"]]);export{w as M};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
import{a as m,_ as h}from"./index-COtE2fCT.js";import{a as u,E as x}from"./vendor-element-B5S5pUKo.js";import{r as p,aj as i,n as T,q as E,t as r,L as a,E as o,I as b}from"./vendor-vue-CVxSw_oJ.js";import"./vendor-axios-B9ygI19o.js";import"./vendor-misc-BeoNyvBp.js";async function P(l){const{data:s}=await m.put("/admin/username",{new_username:l});return s}async function C(l){const{data:s}=await m.put("/admin/password",{new_password:l});return s}async function S(){const{data:l}=await m.post("/logout");return l}const U={class:"page-stack"},A={__name:"SettingsPage",setup(l){const s=p(""),d=p(""),n=p(!1);function k(t){const e=String(t||"");return e.length<8?{ok:!1,message:"密码长度至少8位"}:e.length>128?{ok:!1,message:"密码长度不能超过128个字符"}:!/[a-zA-Z]/.test(e)||!/\d/.test(e)?{ok:!1,message:"密码必须包含字母和数字"}:{ok:!0,message:""}}async function f(){try{await S()}catch{}finally{window.location.href="/yuyx"}}async function V(){const t=s.value.trim();if(!t){u.error("请输入新用户名");return}try{await x.confirm(`确定将管理员用户名修改为「${t}」吗?修改后需要重新登录。`,"修改用户名",{confirmButtonText:"确认修改",cancelButtonText:"取消",type:"warning"})}catch{return}n.value=!0;try{await P(t),u.success("用户名修改成功,请重新登录"),s.value="",setTimeout(f,1200)}catch{}finally{n.value=!1}}async function B(){const t=d.value;if(!t){u.error("请输入新密码");return}const e=k(t);if(!e.ok){u.error(e.message);return}try{await x.confirm("确定修改管理员密码吗?修改后需要重新登录。","修改密码",{confirmButtonText:"确认修改",cancelButtonText:"取消",type:"warning"})}catch{return}n.value=!0;try{await C(t),u.success("密码修改成功,请重新登录"),d.value="",setTimeout(f,1200)}catch{}finally{n.value=!1}}return(t,e)=>{const g=i("el-input"),v=i("el-form-item"),w=i("el-form"),y=i("el-button"),_=i("el-card");return E(),T("div",U,[e[7]||(e[7]=r("div",{class:"app-page-title"},[r("h2",null,"设置"),r("span",{class:"app-muted"},"管理员账号设置")],-1)),a(_,{shadow:"never","body-style":{padding:"16px"},class:"card"},{default:o(()=>[e[3]||(e[3]=r("h3",{class:"section-title"},"修改管理员用户名",-1)),a(w,{"label-width":"120px"},{default:o(()=>[a(v,{label:"新用户名"},{default:o(()=>[a(g,{modelValue:s.value,"onUpdate:modelValue":e[0]||(e[0]=c=>s.value=c),placeholder:"输入新用户名",disabled:n.value},null,8,["modelValue","disabled"])]),_:1})]),_:1}),a(y,{type:"primary",loading:n.value,onClick:V},{default:o(()=>[...e[2]||(e[2]=[b("保存用户名",-1)])]),_:1},8,["loading"])]),_:1}),a(_,{shadow:"never","body-style":{padding:"16px"},class:"card"},{default:o(()=>[e[5]||(e[5]=r("h3",{class:"section-title"},"修改管理员密码",-1)),a(w,{"label-width":"120px"},{default:o(()=>[a(v,{label:"新密码"},{default:o(()=>[a(g,{modelValue:d.value,"onUpdate:modelValue":e[1]||(e[1]=c=>d.value=c),type:"password","show-password":"",placeholder:"输入新密码",disabled:n.value},null,8,["modelValue","disabled"])]),_:1})]),_:1}),a(y,{type:"primary",loading:n.value,onClick:B},{default:o(()=>[...e[4]||(e[4]=[b("保存密码",-1)])]),_:1},8,["loading"]),e[6]||(e[6]=r("div",{class:"help"},"建议使用更强密码(至少8位且包含字母与数字)。",-1))]),_:1})])}}},z=h(A,[["__scopeId","data-v-83d3840a"]]);export{z as default};
@@ -0,0 +1 @@
import{a as g,_ as T}from"./index-BsqM_wut.js";import{a as d,E as x}from"./vendor-element-B5S5pUKo.js";import{r as f,aj as w,n as S,q as U,t as m,L as a,E as s,I as P}from"./vendor-vue-CVxSw_oJ.js";import"./vendor-axios-B9ygI19o.js";import"./vendor-misc-BeoNyvBp.js";async function E(n){const{data:o}=await g.put("/admin/username",{new_username:n});return o}async function C(n={}){const o=String(n.currentPassword||""),i=String(n.newPassword||""),{data:c}=await g.put("/admin/password",{current_password:o,new_password:i});return c}async function A(){const{data:n}=await g.post("/logout");return n}const N={class:"page-stack"},I={__name:"SettingsPage",setup(n){const o=f(""),i=f(""),c=f(""),v=f(""),t=f(!1);function k(l){const e=String(l||"");return e.length<8?{ok:!1,message:"密码长度至少8位"}:e.length>128?{ok:!1,message:"密码长度不能超过128个字符"}:!/[a-zA-Z]/.test(e)||!/\d/.test(e)?{ok:!1,message:"密码必须包含字母和数字"}:{ok:!0,message:""}}async function y(){try{await A()}catch{}finally{window.location.href="/yuyx"}}async function B(){const l=o.value.trim();if(!l){d.error("请输入新用户名");return}try{await x.confirm(`确定将管理员用户名修改为「${l}」吗?修改后需要重新登录。`,"修改用户名",{confirmButtonText:"确认修改",cancelButtonText:"取消",type:"warning"})}catch{return}t.value=!0;try{await E(l),d.success("用户名修改成功,请重新登录"),o.value="",setTimeout(y,1200)}catch{}finally{t.value=!1}}async function h(){const l=i.value,e=c.value,p=v.value;if(!l){d.error("请输入当前密码");return}if(!e){d.error("请输入新密码");return}const r=k(e);if(!r.ok){d.error(r.message);return}if(e!==p){d.error("两次输入的新密码不一致");return}try{await x.confirm("确定修改管理员密码吗?修改后需要重新登录。","修改密码",{confirmButtonText:"确认修改",cancelButtonText:"取消",type:"warning"})}catch{return}t.value=!0;try{await C({currentPassword:l,newPassword:e}),d.success("密码修改成功,请重新登录"),i.value="",c.value="",v.value="",setTimeout(y,1200)}catch{}finally{t.value=!1}}return(l,e)=>{const p=w("el-input"),r=w("el-form-item"),_=w("el-form"),b=w("el-button"),V=w("el-card");return U(),S("div",N,[e[9]||(e[9]=m("div",{class:"app-page-title"},[m("h2",null,"设置"),m("span",{class:"app-muted"},"管理员账号设置")],-1)),a(V,{shadow:"never","body-style":{padding:"16px"},class:"card"},{default:s(()=>[e[5]||(e[5]=m("h3",{class:"section-title"},"修改管理员用户名",-1)),a(_,{"label-width":"120px"},{default:s(()=>[a(r,{label:"新用户名"},{default:s(()=>[a(p,{modelValue:o.value,"onUpdate:modelValue":e[0]||(e[0]=u=>o.value=u),placeholder:"输入新用户名",disabled:t.value},null,8,["modelValue","disabled"])]),_:1})]),_:1}),a(b,{type:"primary",loading:t.value,onClick:B},{default:s(()=>[...e[4]||(e[4]=[P("保存用户名",-1)])]),_:1},8,["loading"])]),_:1}),a(V,{shadow:"never","body-style":{padding:"16px"},class:"card"},{default:s(()=>[e[7]||(e[7]=m("h3",{class:"section-title"},"修改管理员密码",-1)),a(_,{"label-width":"120px"},{default:s(()=>[a(r,{label:"当前密码"},{default:s(()=>[a(p,{modelValue:i.value,"onUpdate:modelValue":e[1]||(e[1]=u=>i.value=u),type:"password","show-password":"",placeholder:"输入当前密码",disabled:t.value},null,8,["modelValue","disabled"])]),_:1}),a(r,{label:"新密码"},{default:s(()=>[a(p,{modelValue:c.value,"onUpdate:modelValue":e[2]||(e[2]=u=>c.value=u),type:"password","show-password":"",placeholder:"输入新密码",disabled:t.value},null,8,["modelValue","disabled"])]),_:1}),a(r,{label:"确认新密码"},{default:s(()=>[a(p,{modelValue:v.value,"onUpdate:modelValue":e[3]||(e[3]=u=>v.value=u),type:"password","show-password":"",placeholder:"再次输入新密码",disabled:t.value},null,8,["modelValue","disabled"])]),_:1})]),_:1}),a(b,{type:"primary",loading:t.value,onClick:h},{default:s(()=>[...e[6]||(e[6]=[P("保存密码",-1)])]),_:1},8,["loading"]),e[8]||(e[8]=m("div",{class:"help"},"建议使用更强密码(至少8位且包含字母与数字)。",-1))]),_:1})])}}},Z=T(I,[["__scopeId","data-v-be652d2b"]]);export{Z as default};
@@ -1 +0,0 @@
.page-stack[data-v-83d3840a]{display:flex;flex-direction:column;gap:14px;min-width:0}.card[data-v-83d3840a]{border-radius:var(--app-radius);border:1px solid var(--app-border);background:var(--app-card-bg);box-shadow:var(--app-shadow-soft)}.section-title[data-v-83d3840a]{margin:0 0 12px;font-size:15px;font-weight:800;letter-spacing:.2px}.help[data-v-83d3840a]{margin-top:10px;font-size:12px;color:var(--app-muted)}
@@ -0,0 +1 @@
.page-stack[data-v-be652d2b]{display:flex;flex-direction:column;gap:14px;min-width:0}.card[data-v-be652d2b]{border-radius:var(--app-radius);border:1px solid var(--app-border);background:var(--app-card-bg);box-shadow:var(--app-shadow-soft)}.section-title[data-v-be652d2b]{margin:0 0 12px;font-size:15px;font-weight:800;letter-spacing:.2px}.help[data-v-be652d2b]{margin-top:10px;font-size:12px;color:var(--app-muted)}
@@ -1,4 +1,4 @@
import{f as be,u as Z}from"./system-CCwKPotj.js";import{a as P,_ as ge}from"./index-COtE2fCT.js";import{E as ue,a as m}from"./vendor-element-B5S5pUKo.js";import{r as n,c as ke,l as xe,R as we,o as Ue,aj as v,ap as Ce,F as Pe,q as V,n as b,t as s,L as l,E as t,I as g,G as ee,J as le}from"./vendor-vue-CVxSw_oJ.js";import"./vendor-axios-B9ygI19o.js";import"./vendor-misc-BeoNyvBp.js";async function ae(r={}){const{data:c}=await P.get("/kdocs/status",{params:r});return c}async function Se(r={}){const c={force:!0,...r},{data:k}=await P.post("/kdocs/qr",c);return k}async function Ie(){const{data:r}=await P.post("/kdocs/clear-login",{});return r}async function Ae(){const{data:r}=await P.get("/proxy/config");return r}async function Ne(r){const{data:c}=await P.post("/proxy/config",r);return c}async function De(r){const{data:c}=await P.post("/proxy/test",r);return c}const Ee={class:"page-stack"},Ke={class:"config-grid"},Le={class:"row-actions"},Qe={class:"row-actions"},qe={class:"row-actions"},Be={class:"section-head"},Te={class:"status-inline app-muted"},$e={key:0},Me={key:1},Re={key:2},Fe={class:"kdocs-inline"},He={class:"kdocs-range"},he={class:"row-actions"},ze={key:0,class:"help"},Ge={key:1,class:"help"},Oe={class:"kdocs-qr"},je=["src"],Je={__name:"SystemPage",setup(r){const c=n(!1),k=n(2),I=n(1),A=n(3),N=n(120),S=n(!1),f=n(""),D=n(3),E=n(!1),K=n(10),L=n(7),Q=n(!1),q=n(""),B=n(""),T=n(""),$=n(0),M=n("A"),R=n("D"),F=n(0),H=n(0),h=n(!1),z=n(""),p=n({}),x=n(!1),w=n(""),oe=n(!1),G=n(!1),U=n(!1),C=n(!1),O=n("");let j=null;const te=ke(()=>G.value||U.value||C.value);function i(a){if(!a){O.value="";return}const e=new Date().toLocaleTimeString("zh-CN",{hour12:!1});O.value=`${a} (${e})`}async function de(){c.value=!0;try{const[a,e,d]=await Promise.all([be(),Ae(),ae().catch(()=>({}))]);k.value=a.max_concurrent_global??2,I.value=a.max_concurrent_per_account??1,A.value=a.max_screenshot_concurrent??3,N.value=a.db_slow_query_ms??120,E.value=(a.auto_approve_enabled??0)===1,K.value=a.auto_approve_hourly_limit??10,L.value=a.auto_approve_vip_days??7,S.value=(e.proxy_enabled??0)===1,f.value=e.proxy_api_url||"",D.value=e.proxy_expire_minutes??3,Q.value=(a.kdocs_enabled??0)===1,q.value=a.kdocs_doc_url||"",B.value=a.kdocs_default_unit||"",T.value=a.kdocs_sheet_name||"",$.value=a.kdocs_sheet_index??0,M.value=(a.kdocs_unit_column||"A").toUpperCase(),R.value=(a.kdocs_image_column||"D").toUpperCase(),F.value=a.kdocs_row_start??0,H.value=a.kdocs_row_end??0,h.value=(a.kdocs_admin_notify_enabled??0)===1,z.value=a.kdocs_admin_notify_email||"",p.value=d||{}}catch{}finally{c.value=!1}}async function ie(){const a={max_concurrent_global:Number(k.value),max_concurrent_per_account:Number(I.value),max_screenshot_concurrent:Number(A.value),db_slow_query_ms:Number(N.value)};try{await ue.confirm(`确定更新并发配置吗? import{f as be,u as Z}from"./system-CAzjuaad.js";import{a as P,_ as ge}from"./index-BsqM_wut.js";import{E as ue,a as m}from"./vendor-element-B5S5pUKo.js";import{r as n,c as ke,l as xe,R as we,o as Ue,aj as v,ap as Ce,F as Pe,q as V,n as b,t as s,L as l,E as t,I as g,G as ee,J as le}from"./vendor-vue-CVxSw_oJ.js";import"./vendor-axios-B9ygI19o.js";import"./vendor-misc-BeoNyvBp.js";async function ae(r={}){const{data:c}=await P.get("/kdocs/status",{params:r});return c}async function Se(r={}){const c={force:!0,...r},{data:k}=await P.post("/kdocs/qr",c);return k}async function Ie(){const{data:r}=await P.post("/kdocs/clear-login",{});return r}async function Ae(){const{data:r}=await P.get("/proxy/config");return r}async function Ne(r){const{data:c}=await P.post("/proxy/config",r);return c}async function De(r){const{data:c}=await P.post("/proxy/test",r);return c}const Ee={class:"page-stack"},Ke={class:"config-grid"},Le={class:"row-actions"},Qe={class:"row-actions"},qe={class:"row-actions"},Be={class:"section-head"},Te={class:"status-inline app-muted"},$e={key:0},Me={key:1},Re={key:2},Fe={class:"kdocs-inline"},He={class:"kdocs-range"},he={class:"row-actions"},ze={key:0,class:"help"},Ge={key:1,class:"help"},Oe={class:"kdocs-qr"},je=["src"],Je={__name:"SystemPage",setup(r){const c=n(!1),k=n(2),I=n(1),A=n(3),N=n(120),S=n(!1),f=n(""),D=n(3),E=n(!1),K=n(10),L=n(7),Q=n(!1),q=n(""),B=n(""),T=n(""),$=n(0),M=n("A"),R=n("D"),F=n(0),H=n(0),h=n(!1),z=n(""),p=n({}),x=n(!1),w=n(""),oe=n(!1),G=n(!1),U=n(!1),C=n(!1),O=n("");let j=null;const te=ke(()=>G.value||U.value||C.value);function i(a){if(!a){O.value="";return}const e=new Date().toLocaleTimeString("zh-CN",{hour12:!1});O.value=`${a} (${e})`}async function de(){c.value=!0;try{const[a,e,d]=await Promise.all([be(),Ae(),ae().catch(()=>({}))]);k.value=a.max_concurrent_global??2,I.value=a.max_concurrent_per_account??1,A.value=a.max_screenshot_concurrent??3,N.value=a.db_slow_query_ms??120,E.value=(a.auto_approve_enabled??0)===1,K.value=a.auto_approve_hourly_limit??10,L.value=a.auto_approve_vip_days??7,S.value=(e.proxy_enabled??0)===1,f.value=e.proxy_api_url||"",D.value=e.proxy_expire_minutes??3,Q.value=(a.kdocs_enabled??0)===1,q.value=a.kdocs_doc_url||"",B.value=a.kdocs_default_unit||"",T.value=a.kdocs_sheet_name||"",$.value=a.kdocs_sheet_index??0,M.value=(a.kdocs_unit_column||"A").toUpperCase(),R.value=(a.kdocs_image_column||"D").toUpperCase(),F.value=a.kdocs_row_start??0,H.value=a.kdocs_row_end??0,h.value=(a.kdocs_admin_notify_enabled??0)===1,z.value=a.kdocs_admin_notify_email||"",p.value=d||{}}catch{}finally{c.value=!1}}async function ie(){const a={max_concurrent_global:Number(k.value),max_concurrent_per_account:Number(I.value),max_screenshot_concurrent:Number(A.value),db_slow_query_ms:Number(N.value)};try{await ue.confirm(`确定更新并发配置吗?
全局并发数: ${a.max_concurrent_global} 全局并发数: ${a.max_concurrent_global}
单账号并发数: ${a.max_concurrent_per_account} 单账号并发数: ${a.max_concurrent_per_account}
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
import{c as s,a as e}from"./index-COtE2fCT.js";const n=s(async()=>{const{data:a}=await e.get("/email/stats");return a},1e4);async function i(){const{data:a}=await e.get("/email/settings");return a}async function r(a){const{data:t}=await e.post("/email/settings",a);return n.clear(),t}async function o(a={}){return n.run(a)}async function l(a){const{data:t}=await e.get("/email/logs",{params:a});return t}async function u(a){const{data:t}=await e.post("/email/logs/cleanup",{days:a});return n.clear(),t}export{l as a,i as b,u as c,o as f,r as u}; import{c as s,a as e}from"./index-BsqM_wut.js";const n=s(async()=>{const{data:a}=await e.get("/email/stats");return a},1e4);async function i(){const{data:a}=await e.get("/email/settings");return a}async function r(a){const{data:t}=await e.post("/email/settings",a);return n.clear(),t}async function o(a={}){return n.run(a)}async function l(a){const{data:t}=await e.get("/email/logs",{params:a});return t}async function u(a){const{data:t}=await e.post("/email/logs/cleanup",{days:a});return n.clear(),t}export{l as a,i as b,u as c,o as f,r as u};
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
import{c as s,a}from"./index-COtE2fCT.js";const e=s(async()=>{const{data:t}=await a.get("/system/config");return t},15e3);async function o(t={}){return e.run(t)}async function r(t){const{data:n}=await a.post("/system/config",t);return e.clear(),n}export{o as f,r as u}; import{c as s,a}from"./index-BsqM_wut.js";const e=s(async()=>{const{data:t}=await a.get("/system/config");return t},15e3);async function o(t={}){return e.run(t)}async function r(t){const{data:n}=await a.post("/system/config",t);return e.clear(),n}export{o as f,r as u};
@@ -1 +1 @@
import{c as s,a}from"./index-COtE2fCT.js";const c=s(async()=>{const{data:t}=await a.get("/server/info");return t},3e4),o=s(async()=>{const{data:t}=await a.get("/docker_stats");return t},8e3),u=s(async()=>{const{data:t}=await a.get("/request_metrics");return t},1e4),i=s(async()=>{const{data:t}=await a.get("/slow_sql_metrics");return t},1e4),e=s(async()=>{const{data:t}=await a.get("/task/stats");return t},4e3),r=s(async()=>{const{data:t}=await a.get("/task/running");return t},2e3);async function g(t={}){return c.run(t)}async function y(t={}){return o.run(t)}async function d(t={}){return u.run(t)}async function k(t={}){return i.run(t)}async function l(t={}){return e.run(t)}async function w(t={}){return r.run(t)}async function _(t){const{data:n}=await a.get("/task/logs",{params:t});return n}async function h(t){const{data:n}=await a.post("/task/logs/clear",{days:t});return e.clear(),r.clear(),n}export{w as a,g as b,y as c,d,k as e,l as f,_ as g,h}; import{c as s,a}from"./index-BsqM_wut.js";const c=s(async()=>{const{data:t}=await a.get("/server/info");return t},3e4),o=s(async()=>{const{data:t}=await a.get("/docker_stats");return t},8e3),u=s(async()=>{const{data:t}=await a.get("/request_metrics");return t},1e4),i=s(async()=>{const{data:t}=await a.get("/slow_sql_metrics");return t},1e4),e=s(async()=>{const{data:t}=await a.get("/task/stats");return t},4e3),r=s(async()=>{const{data:t}=await a.get("/task/running");return t},2e3);async function g(t={}){return c.run(t)}async function y(t={}){return o.run(t)}async function d(t={}){return u.run(t)}async function k(t={}){return i.run(t)}async function l(t={}){return e.run(t)}async function w(t={}){return r.run(t)}async function _(t){const{data:n}=await a.get("/task/logs",{params:t});return n}async function h(t){const{data:n}=await a.post("/task/logs/clear",{days:t});return e.clear(),r.clear(),n}export{w as a,g as b,y as c,d,k as e,l as f,_ as g,h};
@@ -1 +1 @@
import{a as t}from"./index-COtE2fCT.js";async function n(){const{data:s}=await t.get("/users");return s}async function o(s){const{data:a}=await t.post(`/users/${s}/approve`);return a}async function c(s){const{data:a}=await t.post(`/users/${s}/reject`);return a}async function i(s){const{data:a}=await t.delete(`/users/${s}`);return a}async function u(s,a){const{data:e}=await t.post(`/users/${s}/vip`,{days:a});return e}async function p(s){const{data:a}=await t.delete(`/users/${s}/vip`);return a}async function d(s,a){const{data:e}=await t.post(`/users/${s}/reset_password`,{new_password:a});return e}export{o as a,p as b,d as c,i as d,n as f,c as r,u as s}; import{a as t}from"./index-BsqM_wut.js";async function n(){const{data:s}=await t.get("/users");return s}async function o(s){const{data:a}=await t.post(`/users/${s}/approve`);return a}async function c(s){const{data:a}=await t.post(`/users/${s}/reject`);return a}async function i(s){const{data:a}=await t.delete(`/users/${s}`);return a}async function u(s,a){const{data:e}=await t.post(`/users/${s}/vip`,{days:a});return e}async function p(s){const{data:a}=await t.delete(`/users/${s}/vip`);return a}async function d(s,a){const{data:e}=await t.post(`/users/${s}/reset_password`,{new_password:a});return e}export{o as a,p as b,d as c,i as d,n as f,c as r,u as s};
+1 -1
View File
@@ -5,7 +5,7 @@
<link rel="icon" type="image/svg+xml" href="./vite.svg" /> <link rel="icon" type="image/svg+xml" href="./vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>后台管理 - 知识管理平台</title> <title>后台管理 - 知识管理平台</title>
<script type="module" crossorigin src="./assets/index-COtE2fCT.js"></script> <script type="module" crossorigin src="./assets/index-BsqM_wut.js"></script>
<link rel="modulepreload" crossorigin href="./assets/vendor-vue-CVxSw_oJ.js"> <link rel="modulepreload" crossorigin href="./assets/vendor-vue-CVxSw_oJ.js">
<link rel="modulepreload" crossorigin href="./assets/vendor-misc-BeoNyvBp.js"> <link rel="modulepreload" crossorigin href="./assets/vendor-misc-BeoNyvBp.js">
<link rel="modulepreload" crossorigin href="./assets/vendor-element-B5S5pUKo.js"> <link rel="modulepreload" crossorigin href="./assets/vendor-element-B5S5pUKo.js">