更新系统页面和更新功能
- 更新 admin-frontend 系统页面和更新 API - 更新 routes 和 services 中的更新逻辑 - 重新构建前端静态资源
This commit is contained in:
@@ -20,8 +20,7 @@ export async function requestUpdateCheck() {
|
|||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function requestUpdateRun() {
|
export async function requestUpdateRun(payload = {}) {
|
||||||
const { data } = await api.post('/update/run', {})
|
const { data } = await api.post('/update/run', payload)
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ const updateStatusError = ref('')
|
|||||||
const updateResult = ref(null)
|
const updateResult = ref(null)
|
||||||
const updateLog = ref('')
|
const updateLog = ref('')
|
||||||
const updateLogTruncated = ref(false)
|
const updateLogTruncated = ref(false)
|
||||||
|
const updateBuildNoCache = ref(false)
|
||||||
let updatePollTimer = null
|
let updatePollTimer = null
|
||||||
|
|
||||||
const weekdaysOptions = [
|
const weekdaysOptions = [
|
||||||
@@ -316,10 +317,11 @@ async function onCheckUpdate() {
|
|||||||
async function onRunUpdate() {
|
async function onRunUpdate() {
|
||||||
const status = updateStatus.value
|
const status = updateStatus.value
|
||||||
const remote = status?.remote_commit ? shortCommit(status.remote_commit) : '-'
|
const remote = status?.remote_commit ? shortCommit(status.remote_commit) : '-'
|
||||||
|
const buildFlags = updateBuildNoCache.value ? '\n\n构建选项: 强制重建(--no-cache)' : ''
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await ElMessageBox.confirm(
|
await ElMessageBox.confirm(
|
||||||
`确定开始“一键更新”吗?\n\n目标版本: ${remote}\n\n更新将会重建并重启服务,页面可能短暂不可用;系统会先备份数据库。`,
|
`确定开始“一键更新”吗?\n\n目标版本: ${remote}${buildFlags}\n\n更新将会重建并重启服务,页面可能短暂不可用;系统会先备份数据库。`,
|
||||||
'一键更新确认',
|
'一键更新确认',
|
||||||
{ confirmButtonText: '开始更新', cancelButtonText: '取消', type: 'warning' },
|
{ confirmButtonText: '开始更新', cancelButtonText: '取消', type: 'warning' },
|
||||||
)
|
)
|
||||||
@@ -329,7 +331,7 @@ async function onRunUpdate() {
|
|||||||
|
|
||||||
updateActionLoading.value = true
|
updateActionLoading.value = true
|
||||||
try {
|
try {
|
||||||
const res = await requestUpdateRun()
|
const res = await requestUpdateRun({ build_no_cache: updateBuildNoCache.value ? 1 : 0 })
|
||||||
ElMessage.success(res?.message || '已提交更新请求')
|
ElMessage.success(res?.message || '已提交更新请求')
|
||||||
startUpdatePolling()
|
startUpdatePolling()
|
||||||
setTimeout(() => loadUpdateInfo(), 800)
|
setTimeout(() => loadUpdateInfo(), 800)
|
||||||
@@ -498,6 +500,11 @@ onBeforeUnmount(stopUpdatePolling)
|
|||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
</el-descriptions>
|
</el-descriptions>
|
||||||
|
|
||||||
|
<div class="row-actions" style="align-items: center">
|
||||||
|
<el-checkbox v-model="updateBuildNoCache">强制重建(--no-cache)</el-checkbox>
|
||||||
|
<div class="help" style="margin-top: 0">依赖变更或构建异常时建议开启(更新会更慢)。</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="row-actions">
|
<div class="row-actions">
|
||||||
<el-button @click="loadUpdateInfo" :disabled="updateActionLoading">刷新更新信息</el-button>
|
<el-button @click="loadUpdateInfo" :disabled="updateActionLoading">刷新更新信息</el-button>
|
||||||
<el-button @click="onCheckUpdate" :loading="updateActionLoading">检查更新</el-button>
|
<el-button @click="onCheckUpdate" :loading="updateActionLoading">检查更新</el-button>
|
||||||
|
|||||||
@@ -43,6 +43,28 @@ def _has_pending_request() -> bool:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_bool_field(data: dict, key: str) -> bool | None:
|
||||||
|
if not isinstance(data, dict) or key not in data:
|
||||||
|
return None
|
||||||
|
value = data.get(key)
|
||||||
|
if isinstance(value, bool):
|
||||||
|
return value
|
||||||
|
if isinstance(value, int):
|
||||||
|
if value in (0, 1):
|
||||||
|
return bool(value)
|
||||||
|
raise ValueError(f"{key} 必须是 0/1 或 true/false")
|
||||||
|
if isinstance(value, str):
|
||||||
|
text = value.strip().lower()
|
||||||
|
if text in ("1", "true", "yes", "y", "on"):
|
||||||
|
return True
|
||||||
|
if text in ("0", "false", "no", "n", "off", ""):
|
||||||
|
return False
|
||||||
|
raise ValueError(f"{key} 必须是 0/1 或 true/false")
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
raise ValueError(f"{key} 必须是 0/1 或 true/false")
|
||||||
|
|
||||||
|
|
||||||
@admin_api_bp.route("/update/status", methods=["GET"])
|
@admin_api_bp.route("/update/status", methods=["GET"])
|
||||||
@admin_required
|
@admin_required
|
||||||
def get_update_status_api():
|
def get_update_status_api():
|
||||||
@@ -127,6 +149,17 @@ def request_update_run_api():
|
|||||||
if _has_pending_request():
|
if _has_pending_request():
|
||||||
return jsonify({"error": "已有更新请求正在处理中,请稍后再试"}), 409
|
return jsonify({"error": "已有更新请求正在处理中,请稍后再试"}), 409
|
||||||
|
|
||||||
|
data = request.json or {}
|
||||||
|
try:
|
||||||
|
build_no_cache = _parse_bool_field(data, "build_no_cache")
|
||||||
|
if build_no_cache is None:
|
||||||
|
build_no_cache = _parse_bool_field(data, "no_cache")
|
||||||
|
build_pull = _parse_bool_field(data, "build_pull")
|
||||||
|
if build_pull is None:
|
||||||
|
build_pull = _parse_bool_field(data, "pull")
|
||||||
|
except ValueError as e:
|
||||||
|
return jsonify({"error": str(e)}), 400
|
||||||
|
|
||||||
job_id = _make_job_id(prefix="upd")
|
job_id = _make_job_id(prefix="upd")
|
||||||
payload = {
|
payload = {
|
||||||
"job_id": job_id,
|
"job_id": job_id,
|
||||||
@@ -134,6 +167,8 @@ def request_update_run_api():
|
|||||||
"requested_at": get_beijing_now().strftime("%Y-%m-%d %H:%M:%S"),
|
"requested_at": get_beijing_now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||||
"requested_by": session.get("admin_username") or "",
|
"requested_by": session.get("admin_username") or "",
|
||||||
"requested_ip": _request_ip(),
|
"requested_ip": _request_ip(),
|
||||||
|
"build_no_cache": bool(build_no_cache) if build_no_cache is not None else False,
|
||||||
|
"build_pull": bool(build_pull) if build_pull is not None else False,
|
||||||
}
|
}
|
||||||
write_json_atomic(get_update_request_path(), payload)
|
write_json_atomic(get_update_request_path(), payload)
|
||||||
return jsonify(
|
return jsonify(
|
||||||
@@ -143,4 +178,3 @@ def request_update_run_api():
|
|||||||
"message": "已提交更新请求,服务将重启(页面可能短暂不可用),请等待1-2分钟后刷新",
|
"message": "已提交更新请求,服务将重启(页面可能短暂不可用),请等待1-2分钟后刷新",
|
||||||
}
|
}
|
||||||
), 200
|
), 200
|
||||||
|
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ class TaskScheduler:
|
|||||||
self._pending = [] # heap: (priority, submitted_at, seq, task)
|
self._pending = [] # heap: (priority, submitted_at, seq, task)
|
||||||
self._pending_by_account = {} # {account_id: task}
|
self._pending_by_account = {} # {account_id: task}
|
||||||
self._seq = 0
|
self._seq = 0
|
||||||
|
self._known_account_ids = set()
|
||||||
|
|
||||||
self._running_global = 0
|
self._running_global = 0
|
||||||
self._running_by_user = {} # {user_id: running_count}
|
self._running_by_user = {} # {user_id: running_count}
|
||||||
@@ -143,6 +144,18 @@ class TaskScheduler:
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
# 最后兜底:清理本调度器提交过的 active_task,避免测试/重启时被“任务已在运行中”误拦截
|
||||||
|
try:
|
||||||
|
with self._cond:
|
||||||
|
known_ids = set(self._known_account_ids) | set(self._pending_by_account.keys())
|
||||||
|
self._pending.clear()
|
||||||
|
self._pending_by_account.clear()
|
||||||
|
self._cond.notify_all()
|
||||||
|
for account_id in known_ids:
|
||||||
|
safe_remove_task(account_id)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
def update_limits(self, max_global: int = None, max_per_user: int = None, max_queue_size: int = None):
|
def update_limits(self, max_global: int = None, max_per_user: int = None, max_queue_size: int = None):
|
||||||
"""动态更新并发/队列上限(不影响已在运行的任务)"""
|
"""动态更新并发/队列上限(不影响已在运行的任务)"""
|
||||||
with self._cond:
|
with self._cond:
|
||||||
@@ -215,6 +228,7 @@ class TaskScheduler:
|
|||||||
done_callback=done_callback,
|
done_callback=done_callback,
|
||||||
)
|
)
|
||||||
self._pending_by_account[account_id] = task
|
self._pending_by_account[account_id] = task
|
||||||
|
self._known_account_ids.add(account_id)
|
||||||
priority = 0 if is_vip else 1
|
priority = 0 if is_vip else 1
|
||||||
heapq.heappush(self._pending, (priority, task.submitted_at, task.seq, task))
|
heapq.heappush(self._pending, (priority, task.submitted_at, task.seq, task))
|
||||||
self._cond.notify_all()
|
self._cond.notify_all()
|
||||||
|
|||||||
@@ -3,22 +3,22 @@
|
|||||||
"file": "assets/datetime-ZCuLLiQt.js",
|
"file": "assets/datetime-ZCuLLiQt.js",
|
||||||
"name": "datetime"
|
"name": "datetime"
|
||||||
},
|
},
|
||||||
"_tasks-CFlwbjTx.js": {
|
"_tasks-C0sohTWS.js": {
|
||||||
"file": "assets/tasks-CFlwbjTx.js",
|
"file": "assets/tasks-C0sohTWS.js",
|
||||||
"name": "tasks",
|
"name": "tasks",
|
||||||
"imports": [
|
"imports": [
|
||||||
"index.html"
|
"index.html"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"_users-B1Ww8zl1.js": {
|
"_users-a9w9WKCH.js": {
|
||||||
"file": "assets/users-B1Ww8zl1.js",
|
"file": "assets/users-a9w9WKCH.js",
|
||||||
"name": "users",
|
"name": "users",
|
||||||
"imports": [
|
"imports": [
|
||||||
"index.html"
|
"index.html"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"index.html": {
|
"index.html": {
|
||||||
"file": "assets/index-CKlvOJnw.js",
|
"file": "assets/index-BkLe6kG5.js",
|
||||||
"name": "index",
|
"name": "index",
|
||||||
"src": "index.html",
|
"src": "index.html",
|
||||||
"isEntry": true,
|
"isEntry": true,
|
||||||
@@ -38,7 +38,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"src/pages/AnnouncementsPage.vue": {
|
"src/pages/AnnouncementsPage.vue": {
|
||||||
"file": "assets/AnnouncementsPage-DptOTv9v.js",
|
"file": "assets/AnnouncementsPage-B6R49DrY.js",
|
||||||
"name": "AnnouncementsPage",
|
"name": "AnnouncementsPage",
|
||||||
"src": "src/pages/AnnouncementsPage.vue",
|
"src": "src/pages/AnnouncementsPage.vue",
|
||||||
"isDynamicEntry": true,
|
"isDynamicEntry": true,
|
||||||
@@ -50,7 +50,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"src/pages/EmailPage.vue": {
|
"src/pages/EmailPage.vue": {
|
||||||
"file": "assets/EmailPage-A0u1uqDL.js",
|
"file": "assets/EmailPage-CSqw8er9.js",
|
||||||
"name": "EmailPage",
|
"name": "EmailPage",
|
||||||
"src": "src/pages/EmailPage.vue",
|
"src": "src/pages/EmailPage.vue",
|
||||||
"isDynamicEntry": true,
|
"isDynamicEntry": true,
|
||||||
@@ -62,7 +62,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"src/pages/FeedbacksPage.vue": {
|
"src/pages/FeedbacksPage.vue": {
|
||||||
"file": "assets/FeedbacksPage-2XhnmXAD.js",
|
"file": "assets/FeedbacksPage-B9rEDaI0.js",
|
||||||
"name": "FeedbacksPage",
|
"name": "FeedbacksPage",
|
||||||
"src": "src/pages/FeedbacksPage.vue",
|
"src": "src/pages/FeedbacksPage.vue",
|
||||||
"isDynamicEntry": true,
|
"isDynamicEntry": true,
|
||||||
@@ -74,13 +74,13 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"src/pages/LogsPage.vue": {
|
"src/pages/LogsPage.vue": {
|
||||||
"file": "assets/LogsPage-C6eCwbXQ.js",
|
"file": "assets/LogsPage-eWUoUxcN.js",
|
||||||
"name": "LogsPage",
|
"name": "LogsPage",
|
||||||
"src": "src/pages/LogsPage.vue",
|
"src": "src/pages/LogsPage.vue",
|
||||||
"isDynamicEntry": true,
|
"isDynamicEntry": true,
|
||||||
"imports": [
|
"imports": [
|
||||||
"_users-B1Ww8zl1.js",
|
"_users-a9w9WKCH.js",
|
||||||
"_tasks-CFlwbjTx.js",
|
"_tasks-C0sohTWS.js",
|
||||||
"index.html"
|
"index.html"
|
||||||
],
|
],
|
||||||
"css": [
|
"css": [
|
||||||
@@ -88,12 +88,12 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"src/pages/PendingPage.vue": {
|
"src/pages/PendingPage.vue": {
|
||||||
"file": "assets/PendingPage-CsinHQq-.js",
|
"file": "assets/PendingPage-BVSAb_Yr.js",
|
||||||
"name": "PendingPage",
|
"name": "PendingPage",
|
||||||
"src": "src/pages/PendingPage.vue",
|
"src": "src/pages/PendingPage.vue",
|
||||||
"isDynamicEntry": true,
|
"isDynamicEntry": true,
|
||||||
"imports": [
|
"imports": [
|
||||||
"_users-B1Ww8zl1.js",
|
"_users-a9w9WKCH.js",
|
||||||
"index.html",
|
"index.html",
|
||||||
"_datetime-ZCuLLiQt.js"
|
"_datetime-ZCuLLiQt.js"
|
||||||
],
|
],
|
||||||
@@ -102,7 +102,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"src/pages/SettingsPage.vue": {
|
"src/pages/SettingsPage.vue": {
|
||||||
"file": "assets/SettingsPage-D9ploTWf.js",
|
"file": "assets/SettingsPage-CVlYx8ta.js",
|
||||||
"name": "SettingsPage",
|
"name": "SettingsPage",
|
||||||
"src": "src/pages/SettingsPage.vue",
|
"src": "src/pages/SettingsPage.vue",
|
||||||
"isDynamicEntry": true,
|
"isDynamicEntry": true,
|
||||||
@@ -114,12 +114,12 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"src/pages/StatsPage.vue": {
|
"src/pages/StatsPage.vue": {
|
||||||
"file": "assets/StatsPage-CxBcYwKE.js",
|
"file": "assets/StatsPage-BRQO0uQE.js",
|
||||||
"name": "StatsPage",
|
"name": "StatsPage",
|
||||||
"src": "src/pages/StatsPage.vue",
|
"src": "src/pages/StatsPage.vue",
|
||||||
"isDynamicEntry": true,
|
"isDynamicEntry": true,
|
||||||
"imports": [
|
"imports": [
|
||||||
"_tasks-CFlwbjTx.js",
|
"_tasks-C0sohTWS.js",
|
||||||
"index.html"
|
"index.html"
|
||||||
],
|
],
|
||||||
"css": [
|
"css": [
|
||||||
@@ -127,7 +127,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"src/pages/SystemPage.vue": {
|
"src/pages/SystemPage.vue": {
|
||||||
"file": "assets/SystemPage-DMNoY1AU.js",
|
"file": "assets/SystemPage-StAFDOuj.js",
|
||||||
"name": "SystemPage",
|
"name": "SystemPage",
|
||||||
"src": "src/pages/SystemPage.vue",
|
"src": "src/pages/SystemPage.vue",
|
||||||
"isDynamicEntry": true,
|
"isDynamicEntry": true,
|
||||||
@@ -135,16 +135,16 @@
|
|||||||
"index.html"
|
"index.html"
|
||||||
],
|
],
|
||||||
"css": [
|
"css": [
|
||||||
"assets/SystemPage-B0TEzUGV.css"
|
"assets/SystemPage-CdLP1b5Y.css"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"src/pages/UsersPage.vue": {
|
"src/pages/UsersPage.vue": {
|
||||||
"file": "assets/UsersPage-BzRNJLnT.js",
|
"file": "assets/UsersPage-DkSQZ5YZ.js",
|
||||||
"name": "UsersPage",
|
"name": "UsersPage",
|
||||||
"src": "src/pages/UsersPage.vue",
|
"src": "src/pages/UsersPage.vue",
|
||||||
"isDynamicEntry": true,
|
"isDynamicEntry": true,
|
||||||
"imports": [
|
"imports": [
|
||||||
"_users-B1Ww8zl1.js",
|
"_users-a9w9WKCH.js",
|
||||||
"_datetime-ZCuLLiQt.js",
|
"_datetime-ZCuLLiQt.js",
|
||||||
"index.html"
|
"index.html"
|
||||||
],
|
],
|
||||||
|
|||||||
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
-1
@@ -1 +1 @@
|
|||||||
import{f as E,a as I,r as A}from"./users-B1Ww8zl1.js";import{_ as M,r as p,o as q,c as W,a as i,b as t,w as a,d,i as T,f as F,e as G,g as f,h as r,j as $,k as x,l as H,t as k,E as m,m as g,n as J,p as K}from"./index-CKlvOJnw.js";import{p as L}from"./datetime-ZCuLLiQt.js";const O={class:"page-stack"},Q={class:"app-page-title"},X={class:"table-wrap"},Y={class:"user-cell"},Z={class:"table-wrap"},ee={__name:"PendingPage",setup(te){const B=T("refreshStats",null),_=T("refreshNavBadges",null),v=p([]),c=p([]),w=p(!1),y=p(!1);function D(s){const e=s?.vip_expire_time;if(!e)return!1;if(String(e).startsWith("2099-12-31"))return!0;const o=L(e);return o?o.getTime()>Date.now():!1}async function j(){w.value=!0;try{v.value=await E()}catch{v.value=[]}finally{w.value=!1}}async function h(){y.value=!0;try{c.value=await F()}catch{c.value=[]}finally{y.value=!1}}async function u(){await Promise.all([j(),h()]),await _?.({pendingResets:c.value.length})}async function N(s){try{await m.confirm(`确定通过用户「${s.username}」的注册申请吗?`,"审核通过",{confirmButtonText:"通过",cancelButtonText:"取消",type:"success"})}catch{return}try{await I(s.id),g.success("用户审核通过"),await u(),await B?.()}catch{}}async function U(s){try{await m.confirm(`确定拒绝用户「${s.username}」的注册申请吗?`,"拒绝申请",{confirmButtonText:"拒绝",cancelButtonText:"取消",type:"warning"})}catch{return}try{await A(s.id),g.success("已拒绝用户"),await u(),await B?.()}catch{}}async function V(s){try{await m.confirm(`确定批准「${s.username}」的密码重置申请吗?`,"批准重置",{confirmButtonText:"批准",cancelButtonText:"取消",type:"success"})}catch{return}try{const e=await J(s.id);g.success(e?.message||"密码重置申请已批准"),await h(),await _?.({pendingResets:c.value.length})}catch{}}async function z(s){try{await m.confirm(`确定拒绝「${s.username}」的密码重置申请吗?`,"拒绝重置",{confirmButtonText:"拒绝",cancelButtonText:"取消",type:"warning"})}catch{return}try{const e=await K(s.id);g.success(e?.message||"密码重置申请已拒绝"),await h(),await _?.({pendingResets:c.value.length})}catch{}}return q(u),(s,e)=>{const o=d("el-button"),l=d("el-table-column"),S=d("el-tag"),R=d("el-table"),C=d("el-card"),P=G("loading");return f(),W("div",O,[i("div",Q,[e[1]||(e[1]=i("h2",null,"待审核",-1)),i("div",null,[t(o,{onClick:u},{default:a(()=>[...e[0]||(e[0]=[r("刷新",-1)])]),_:1})])]),t(C,{shadow:"never","body-style":{padding:"16px"},class:"card"},{default:a(()=>[e[5]||(e[5]=i("h3",{class:"section-title"},"用户注册审核",-1)),i("div",X,[$((f(),x(R,{data:v.value,style:{width:"100%"}},{default:a(()=>[t(l,{prop:"id",label:"ID",width:"80"}),t(l,{label:"用户名","min-width":"200"},{default:a(({row:n})=>[i("div",Y,[i("strong",null,k(n.username),1),D(n)?(f(),x(S,{key:0,type:"warning",effect:"light",size:"small"},{default:a(()=>[...e[2]||(e[2]=[r("VIP",-1)])]),_:1})):H("",!0)])]),_:1}),t(l,{prop:"email",label:"邮箱","min-width":"220"},{default:a(({row:n})=>[r(k(n.email||"-"),1)]),_:1}),t(l,{prop:"created_at",label:"注册时间","min-width":"180"}),t(l,{label:"操作",width:"180",fixed:"right"},{default:a(({row:n})=>[t(o,{type:"success",size:"small",onClick:b=>N(n)},{default:a(()=>[...e[3]||(e[3]=[r("通过",-1)])]),_:1},8,["onClick"]),t(o,{type:"danger",size:"small",onClick:b=>U(n)},{default:a(()=>[...e[4]||(e[4]=[r("拒绝",-1)])]),_:1},8,["onClick"])]),_:1})]),_:1},8,["data"])),[[P,w.value]])])]),_:1}),t(C,{shadow:"never","body-style":{padding:"16px"},class:"card"},{default:a(()=>[e[8]||(e[8]=i("h3",{class:"section-title"},"密码重置审核",-1)),i("div",Z,[$((f(),x(R,{data:c.value,style:{width:"100%"}},{default:a(()=>[t(l,{prop:"id",label:"申请ID",width:"90"}),t(l,{prop:"username",label:"用户名","min-width":"200"}),t(l,{prop:"email",label:"邮箱","min-width":"220"},{default:a(({row:n})=>[r(k(n.email||"-"),1)]),_:1}),t(l,{prop:"created_at",label:"申请时间","min-width":"180"}),t(l,{label:"操作",width:"180",fixed:"right"},{default:a(({row:n})=>[t(o,{type:"success",size:"small",onClick:b=>V(n)},{default:a(()=>[...e[6]||(e[6]=[r("批准",-1)])]),_:1},8,["onClick"]),t(o,{type:"danger",size:"small",onClick:b=>z(n)},{default:a(()=>[...e[7]||(e[7]=[r("拒绝",-1)])]),_:1},8,["onClick"])]),_:1})]),_:1},8,["data"])),[[P,y.value]])])]),_:1})])}}},le=M(ee,[["__scopeId","data-v-f2aa6820"]]);export{le as default};
|
import{f as E,a as I,r as A}from"./users-a9w9WKCH.js";import{_ as M,r as p,o as q,c as W,a as i,b as t,w as a,d,i as T,f as F,e as G,g as f,h as r,j as $,k as x,l as H,t as k,E as m,m as g,n as J,p as K}from"./index-BkLe6kG5.js";import{p as L}from"./datetime-ZCuLLiQt.js";const O={class:"page-stack"},Q={class:"app-page-title"},X={class:"table-wrap"},Y={class:"user-cell"},Z={class:"table-wrap"},ee={__name:"PendingPage",setup(te){const B=T("refreshStats",null),_=T("refreshNavBadges",null),v=p([]),c=p([]),w=p(!1),y=p(!1);function D(s){const e=s?.vip_expire_time;if(!e)return!1;if(String(e).startsWith("2099-12-31"))return!0;const o=L(e);return o?o.getTime()>Date.now():!1}async function j(){w.value=!0;try{v.value=await E()}catch{v.value=[]}finally{w.value=!1}}async function h(){y.value=!0;try{c.value=await F()}catch{c.value=[]}finally{y.value=!1}}async function u(){await Promise.all([j(),h()]),await _?.({pendingResets:c.value.length})}async function N(s){try{await m.confirm(`确定通过用户「${s.username}」的注册申请吗?`,"审核通过",{confirmButtonText:"通过",cancelButtonText:"取消",type:"success"})}catch{return}try{await I(s.id),g.success("用户审核通过"),await u(),await B?.()}catch{}}async function U(s){try{await m.confirm(`确定拒绝用户「${s.username}」的注册申请吗?`,"拒绝申请",{confirmButtonText:"拒绝",cancelButtonText:"取消",type:"warning"})}catch{return}try{await A(s.id),g.success("已拒绝用户"),await u(),await B?.()}catch{}}async function V(s){try{await m.confirm(`确定批准「${s.username}」的密码重置申请吗?`,"批准重置",{confirmButtonText:"批准",cancelButtonText:"取消",type:"success"})}catch{return}try{const e=await J(s.id);g.success(e?.message||"密码重置申请已批准"),await h(),await _?.({pendingResets:c.value.length})}catch{}}async function z(s){try{await m.confirm(`确定拒绝「${s.username}」的密码重置申请吗?`,"拒绝重置",{confirmButtonText:"拒绝",cancelButtonText:"取消",type:"warning"})}catch{return}try{const e=await K(s.id);g.success(e?.message||"密码重置申请已拒绝"),await h(),await _?.({pendingResets:c.value.length})}catch{}}return q(u),(s,e)=>{const o=d("el-button"),l=d("el-table-column"),S=d("el-tag"),R=d("el-table"),C=d("el-card"),P=G("loading");return f(),W("div",O,[i("div",Q,[e[1]||(e[1]=i("h2",null,"待审核",-1)),i("div",null,[t(o,{onClick:u},{default:a(()=>[...e[0]||(e[0]=[r("刷新",-1)])]),_:1})])]),t(C,{shadow:"never","body-style":{padding:"16px"},class:"card"},{default:a(()=>[e[5]||(e[5]=i("h3",{class:"section-title"},"用户注册审核",-1)),i("div",X,[$((f(),x(R,{data:v.value,style:{width:"100%"}},{default:a(()=>[t(l,{prop:"id",label:"ID",width:"80"}),t(l,{label:"用户名","min-width":"200"},{default:a(({row:n})=>[i("div",Y,[i("strong",null,k(n.username),1),D(n)?(f(),x(S,{key:0,type:"warning",effect:"light",size:"small"},{default:a(()=>[...e[2]||(e[2]=[r("VIP",-1)])]),_:1})):H("",!0)])]),_:1}),t(l,{prop:"email",label:"邮箱","min-width":"220"},{default:a(({row:n})=>[r(k(n.email||"-"),1)]),_:1}),t(l,{prop:"created_at",label:"注册时间","min-width":"180"}),t(l,{label:"操作",width:"180",fixed:"right"},{default:a(({row:n})=>[t(o,{type:"success",size:"small",onClick:b=>N(n)},{default:a(()=>[...e[3]||(e[3]=[r("通过",-1)])]),_:1},8,["onClick"]),t(o,{type:"danger",size:"small",onClick:b=>U(n)},{default:a(()=>[...e[4]||(e[4]=[r("拒绝",-1)])]),_:1},8,["onClick"])]),_:1})]),_:1},8,["data"])),[[P,w.value]])])]),_:1}),t(C,{shadow:"never","body-style":{padding:"16px"},class:"card"},{default:a(()=>[e[8]||(e[8]=i("h3",{class:"section-title"},"密码重置审核",-1)),i("div",Z,[$((f(),x(R,{data:c.value,style:{width:"100%"}},{default:a(()=>[t(l,{prop:"id",label:"申请ID",width:"90"}),t(l,{prop:"username",label:"用户名","min-width":"200"}),t(l,{prop:"email",label:"邮箱","min-width":"220"},{default:a(({row:n})=>[r(k(n.email||"-"),1)]),_:1}),t(l,{prop:"created_at",label:"申请时间","min-width":"180"}),t(l,{label:"操作",width:"180",fixed:"right"},{default:a(({row:n})=>[t(o,{type:"success",size:"small",onClick:b=>V(n)},{default:a(()=>[...e[6]||(e[6]=[r("批准",-1)])]),_:1},8,["onClick"]),t(o,{type:"danger",size:"small",onClick:b=>z(n)},{default:a(()=>[...e[7]||(e[7]=[r("拒绝",-1)])]),_:1},8,["onClick"])]),_:1})]),_:1},8,["data"])),[[P,y.value]])])]),_:1})])}}},le=M(ee,[["__scopeId","data-v-f2aa6820"]]);export{le as default};
|
||||||
+1
-1
@@ -1 +1 @@
|
|||||||
import{B as m,_ as T,r as p,c as h,a as r,b as a,w as s,d as u,g as k,h as b,m as d,E as x}from"./index-CKlvOJnw.js";async function C(o){const{data:t}=await m.put("/admin/username",{new_username:o});return t}async function E(o){const{data:t}=await m.put("/admin/password",{new_password:o});return t}async function P(){const{data:o}=await m.post("/logout");return o}const U={class:"page-stack"},N={__name:"SettingsPage",setup(o){const t=p(""),i=p(""),n=p(!1);async function f(){try{await P()}catch{}finally{window.location.href="/yuyx"}}async function B(){const l=t.value.trim();if(!l){d.error("请输入新用户名");return}try{await x.confirm(`确定将管理员用户名修改为「${l}」吗?修改后需要重新登录。`,"修改用户名",{confirmButtonText:"确认修改",cancelButtonText:"取消",type:"warning"})}catch{return}n.value=!0;try{await C(l),d.success("用户名修改成功,请重新登录"),t.value="",setTimeout(f,1200)}catch{}finally{n.value=!1}}async function V(){const l=i.value;if(!l){d.error("请输入新密码");return}if(l.length<6){d.error("密码至少6个字符");return}try{await x.confirm("确定修改管理员密码吗?修改后需要重新登录。","修改密码",{confirmButtonText:"确认修改",cancelButtonText:"取消",type:"warning"})}catch{return}n.value=!0;try{await E(l),d.success("密码修改成功,请重新登录"),i.value="",setTimeout(f,1200)}catch{}finally{n.value=!1}}return(l,e)=>{const w=u("el-input"),v=u("el-form-item"),y=u("el-form"),_=u("el-button"),g=u("el-card");return k(),h("div",U,[e[7]||(e[7]=r("div",{class:"app-page-title"},[r("h2",null,"设置"),r("span",{class:"app-muted"},"管理员账号设置")],-1)),a(g,{shadow:"never","body-style":{padding:"16px"},class:"card"},{default:s(()=>[e[3]||(e[3]=r("h3",{class:"section-title"},"修改管理员用户名",-1)),a(y,{"label-width":"120px"},{default:s(()=>[a(v,{label:"新用户名"},{default:s(()=>[a(w,{modelValue:t.value,"onUpdate:modelValue":e[0]||(e[0]=c=>t.value=c),placeholder:"输入新用户名",disabled:n.value},null,8,["modelValue","disabled"])]),_:1})]),_:1}),a(_,{type:"primary",loading:n.value,onClick:B},{default:s(()=>[...e[2]||(e[2]=[b("保存用户名",-1)])]),_:1},8,["loading"])]),_:1}),a(g,{shadow:"never","body-style":{padding:"16px"},class:"card"},{default:s(()=>[e[5]||(e[5]=r("h3",{class:"section-title"},"修改管理员密码",-1)),a(y,{"label-width":"120px"},{default:s(()=>[a(v,{label:"新密码"},{default:s(()=>[a(w,{modelValue:i.value,"onUpdate:modelValue":e[1]||(e[1]=c=>i.value=c),type:"password","show-password":"",placeholder:"输入新密码",disabled:n.value},null,8,["modelValue","disabled"])]),_:1})]),_:1}),a(_,{type:"primary",loading:n.value,onClick:V},{default:s(()=>[...e[4]||(e[4]=[b("保存密码",-1)])]),_:1},8,["loading"]),e[6]||(e[6]=r("div",{class:"help"},"建议使用更强密码(至少8位且包含字母与数字)。",-1))]),_:1})])}}},M=T(N,[["__scopeId","data-v-2f4b840f"]]);export{M as default};
|
import{B as m,_ as T,r as p,c as h,a as r,b as a,w as s,d as u,g as k,h as b,m as d,E as x}from"./index-BkLe6kG5.js";async function C(o){const{data:t}=await m.put("/admin/username",{new_username:o});return t}async function E(o){const{data:t}=await m.put("/admin/password",{new_password:o});return t}async function P(){const{data:o}=await m.post("/logout");return o}const U={class:"page-stack"},N={__name:"SettingsPage",setup(o){const t=p(""),i=p(""),n=p(!1);async function f(){try{await P()}catch{}finally{window.location.href="/yuyx"}}async function B(){const l=t.value.trim();if(!l){d.error("请输入新用户名");return}try{await x.confirm(`确定将管理员用户名修改为「${l}」吗?修改后需要重新登录。`,"修改用户名",{confirmButtonText:"确认修改",cancelButtonText:"取消",type:"warning"})}catch{return}n.value=!0;try{await C(l),d.success("用户名修改成功,请重新登录"),t.value="",setTimeout(f,1200)}catch{}finally{n.value=!1}}async function V(){const l=i.value;if(!l){d.error("请输入新密码");return}if(l.length<6){d.error("密码至少6个字符");return}try{await x.confirm("确定修改管理员密码吗?修改后需要重新登录。","修改密码",{confirmButtonText:"确认修改",cancelButtonText:"取消",type:"warning"})}catch{return}n.value=!0;try{await E(l),d.success("密码修改成功,请重新登录"),i.value="",setTimeout(f,1200)}catch{}finally{n.value=!1}}return(l,e)=>{const w=u("el-input"),v=u("el-form-item"),y=u("el-form"),_=u("el-button"),g=u("el-card");return k(),h("div",U,[e[7]||(e[7]=r("div",{class:"app-page-title"},[r("h2",null,"设置"),r("span",{class:"app-muted"},"管理员账号设置")],-1)),a(g,{shadow:"never","body-style":{padding:"16px"},class:"card"},{default:s(()=>[e[3]||(e[3]=r("h3",{class:"section-title"},"修改管理员用户名",-1)),a(y,{"label-width":"120px"},{default:s(()=>[a(v,{label:"新用户名"},{default:s(()=>[a(w,{modelValue:t.value,"onUpdate:modelValue":e[0]||(e[0]=c=>t.value=c),placeholder:"输入新用户名",disabled:n.value},null,8,["modelValue","disabled"])]),_:1})]),_:1}),a(_,{type:"primary",loading:n.value,onClick:B},{default:s(()=>[...e[2]||(e[2]=[b("保存用户名",-1)])]),_:1},8,["loading"])]),_:1}),a(g,{shadow:"never","body-style":{padding:"16px"},class:"card"},{default:s(()=>[e[5]||(e[5]=r("h3",{class:"section-title"},"修改管理员密码",-1)),a(y,{"label-width":"120px"},{default:s(()=>[a(v,{label:"新密码"},{default:s(()=>[a(w,{modelValue:i.value,"onUpdate:modelValue":e[1]||(e[1]=c=>i.value=c),type:"password","show-password":"",placeholder:"输入新密码",disabled:n.value},null,8,["modelValue","disabled"])]),_:1})]),_:1}),a(_,{type:"primary",loading:n.value,onClick:V},{default:s(()=>[...e[4]||(e[4]=[b("保存密码",-1)])]),_:1},8,["loading"]),e[6]||(e[6]=r("div",{class:"help"},"建议使用更强密码(至少8位且包含字母与数字)。",-1))]),_:1})])}}},M=T(N,[["__scopeId","data-v-2f4b840f"]]);export{M as default};
|
||||||
+1
-1
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
|||||||
.page-stack[data-v-b35a5b19]{display:flex;flex-direction:column;gap:12px}.card[data-v-b35a5b19]{border-radius:var(--app-radius);border:1px solid var(--app-border)}.section-title[data-v-b35a5b19]{margin:0 0 12px;font-size:14px;font-weight:800}.help[data-v-b35a5b19]{margin-top:6px;font-size:12px;color:var(--app-muted)}.row-actions[data-v-b35a5b19]{display:flex;flex-wrap:wrap;gap:10px}
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
.page-stack[data-v-3b98b1dc]{display:flex;flex-direction:column;gap:12px}.card[data-v-3b98b1dc]{border-radius:var(--app-radius);border:1px solid var(--app-border)}.section-title[data-v-3b98b1dc]{margin:0 0 12px;font-size:14px;font-weight:800}.help[data-v-3b98b1dc]{margin-top:6px;font-size:12px;color:var(--app-muted)}.row-actions[data-v-3b98b1dc]{display:flex;flex-wrap:wrap;gap:10px}
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
-1
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{B as a}from"./index-CKlvOJnw.js";async function c(){const{data:t}=await a.get("/server/info");return t}async function e(){const{data:t}=await a.get("/docker_stats");return t}async function o(){const{data:t}=await a.get("/task/stats");return t}async function r(){const{data:t}=await a.get("/task/running");return t}async function i(t){const{data:s}=await a.get("/task/logs",{params:t});return s}async function f(t){const{data:s}=await a.post("/task/logs/clear",{days:t});return s}export{e as a,o as b,r as c,i as d,f as e,c as f};
|
import{B as a}from"./index-BkLe6kG5.js";async function c(){const{data:t}=await a.get("/server/info");return t}async function e(){const{data:t}=await a.get("/docker_stats");return t}async function o(){const{data:t}=await a.get("/task/stats");return t}async function r(){const{data:t}=await a.get("/task/running");return t}async function i(t){const{data:s}=await a.get("/task/logs",{params:t});return s}async function f(t){const{data:s}=await a.post("/task/logs/clear",{days:t});return s}export{e as a,o as b,r as c,i as d,f as e,c as f};
|
||||||
@@ -1 +1 @@
|
|||||||
import{B as a}from"./index-CKlvOJnw.js";async function r(){const{data:s}=await a.get("/users");return s}async function c(){const{data:s}=await a.get("/users/pending");return s}async function o(s){const{data:t}=await a.post(`/users/${s}/approve`);return t}async function i(s){const{data:t}=await a.post(`/users/${s}/reject`);return t}async function u(s){const{data:t}=await a.delete(`/users/${s}`);return t}async function d(s,t){const{data:e}=await a.post(`/users/${s}/vip`,{days:t});return e}async function p(s){const{data:t}=await a.delete(`/users/${s}/vip`);return t}async function f(s,t){const{data:e}=await a.post(`/users/${s}/reset_password`,{new_password:t});return e}export{o as a,r as b,p as c,f as d,u as e,c as f,i as r,d as s};
|
import{B as a}from"./index-BkLe6kG5.js";async function r(){const{data:s}=await a.get("/users");return s}async function c(){const{data:s}=await a.get("/users/pending");return s}async function o(s){const{data:t}=await a.post(`/users/${s}/approve`);return t}async function i(s){const{data:t}=await a.post(`/users/${s}/reject`);return t}async function u(s){const{data:t}=await a.delete(`/users/${s}`);return t}async function d(s,t){const{data:e}=await a.post(`/users/${s}/vip`,{days:t});return e}async function p(s){const{data:t}=await a.delete(`/users/${s}/vip`);return t}async function f(s,t){const{data:e}=await a.post(`/users/${s}/reset_password`,{new_password:t});return e}export{o as a,r as b,p as c,f as d,u as e,c as f,i as r,d as s};
|
||||||
@@ -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-CKlvOJnw.js"></script>
|
<script type="module" crossorigin src="./assets/index-BkLe6kG5.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="./assets/index-BIDpnzAs.css">
|
<link rel="stylesheet" crossorigin href="./assets/index-BIDpnzAs.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
+49
-2
@@ -66,6 +66,15 @@ def sanitize_job_id(value: object) -> str:
|
|||||||
return text
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
def _as_bool(value: object) -> bool:
|
||||||
|
if isinstance(value, bool):
|
||||||
|
return value
|
||||||
|
if isinstance(value, int):
|
||||||
|
return value != 0
|
||||||
|
text = str(value or "").strip().lower()
|
||||||
|
return text in ("1", "true", "yes", "y", "on")
|
||||||
|
|
||||||
|
|
||||||
def _run(cmd: list[str], *, cwd: Path, log_fp, env: Optional[dict] = None, check: bool = True) -> subprocess.CompletedProcess:
|
def _run(cmd: list[str], *, cwd: Path, log_fp, env: Optional[dict] = None, check: bool = True) -> subprocess.CompletedProcess:
|
||||||
log_fp.write(f"[{ts_str()}] $ {' '.join(cmd)}\n")
|
log_fp.write(f"[{ts_str()}] $ {' '.join(cmd)}\n")
|
||||||
log_fp.flush()
|
log_fp.flush()
|
||||||
@@ -305,7 +314,16 @@ def consume_request(paths: Paths) -> Tuple[dict, Optional[str]]:
|
|||||||
return data, None
|
return data, None
|
||||||
|
|
||||||
|
|
||||||
def handle_update_job(*, paths: Paths, branch: str, health_url: str, job_id: str, requested_by: str) -> None:
|
def handle_update_job(
|
||||||
|
*,
|
||||||
|
paths: Paths,
|
||||||
|
branch: str,
|
||||||
|
health_url: str,
|
||||||
|
job_id: str,
|
||||||
|
requested_by: str,
|
||||||
|
build_no_cache: bool = False,
|
||||||
|
build_pull: bool = False,
|
||||||
|
) -> None:
|
||||||
ensure_dirs(paths)
|
ensure_dirs(paths)
|
||||||
log_path = paths.jobs_dir / f"{job_id}.log"
|
log_path = paths.jobs_dir / f"{job_id}.log"
|
||||||
with open(log_path, "a", encoding="utf-8") as log_fp:
|
with open(log_path, "a", encoding="utf-8") as log_fp:
|
||||||
@@ -323,6 +341,8 @@ def handle_update_job(*, paths: Paths, branch: str, health_url: str, job_id: str
|
|||||||
"duration_seconds": None,
|
"duration_seconds": None,
|
||||||
"requested_by": requested_by,
|
"requested_by": requested_by,
|
||||||
"branch": branch,
|
"branch": branch,
|
||||||
|
"build_no_cache": bool(build_no_cache),
|
||||||
|
"build_pull": bool(build_pull),
|
||||||
"from_commit": None,
|
"from_commit": None,
|
||||||
"to_commit": None,
|
"to_commit": None,
|
||||||
"backup_db": None,
|
"backup_db": None,
|
||||||
@@ -360,7 +380,30 @@ def handle_update_job(*, paths: Paths, branch: str, health_url: str, job_id: str
|
|||||||
result["stage"] = "docker_build"
|
result["stage"] = "docker_build"
|
||||||
result["message"] = "构建容器镜像"
|
result["message"] = "构建容器镜像"
|
||||||
write_result(paths, result)
|
write_result(paths, result)
|
||||||
_run([*compose, "build"], cwd=paths.repo_dir, log_fp=log_fp)
|
build_no_cache = bool(result.get("build_no_cache") is True)
|
||||||
|
build_pull = bool(result.get("build_pull") is True)
|
||||||
|
|
||||||
|
build_cmd = [*compose, "build"]
|
||||||
|
if build_pull:
|
||||||
|
build_cmd.append("--pull")
|
||||||
|
if build_no_cache:
|
||||||
|
build_cmd.append("--no-cache")
|
||||||
|
try:
|
||||||
|
_run(build_cmd, cwd=paths.repo_dir, log_fp=log_fp)
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
if (not build_no_cache) and (e.returncode != 0):
|
||||||
|
log_fp.write(f"[{ts_str()}] build failed, retry with --no-cache\n")
|
||||||
|
log_fp.flush()
|
||||||
|
build_no_cache = True
|
||||||
|
result["build_no_cache"] = True
|
||||||
|
write_result(paths, result)
|
||||||
|
retry_cmd = [*compose, "build"]
|
||||||
|
if build_pull:
|
||||||
|
retry_cmd.append("--pull")
|
||||||
|
retry_cmd.append("--no-cache")
|
||||||
|
_run(retry_cmd, cwd=paths.repo_dir, log_fp=log_fp)
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
|
||||||
result["stage"] = "docker_up"
|
result["stage"] = "docker_up"
|
||||||
result["message"] = "重建并启动服务"
|
result["message"] = "重建并启动服务"
|
||||||
@@ -501,12 +544,16 @@ def main(argv: list[str]) -> int:
|
|||||||
elif action == "check":
|
elif action == "check":
|
||||||
handle_check_job(paths=paths, branch=branch, job_id=job_id, requested_by=requested_by)
|
handle_check_job(paths=paths, branch=branch, job_id=job_id, requested_by=requested_by)
|
||||||
else:
|
else:
|
||||||
|
build_no_cache = _as_bool(req.get("build_no_cache") or req.get("no_cache") or False)
|
||||||
|
build_pull = _as_bool(req.get("build_pull") or req.get("pull") or False)
|
||||||
handle_update_job(
|
handle_update_job(
|
||||||
paths=paths,
|
paths=paths,
|
||||||
branch=branch,
|
branch=branch,
|
||||||
health_url=health_url,
|
health_url=health_url,
|
||||||
job_id=job_id,
|
job_id=job_id,
|
||||||
requested_by=requested_by,
|
requested_by=requested_by,
|
||||||
|
build_no_cache=build_no_cache,
|
||||||
|
build_pull=build_pull,
|
||||||
)
|
)
|
||||||
|
|
||||||
last_check_ts = time.time()
|
last_check_ts = time.time()
|
||||||
|
|||||||
Reference in New Issue
Block a user