feat: 用户权限系统(权限位/管理员全权限/前端隐藏/最后管理员保护)
This commit is contained in:
+27
-2
@@ -45,11 +45,35 @@ def _sqlite_pragma(dbapi_connection, connection_record):
|
||||
|
||||
|
||||
class User(UserMixin, db.Model):
|
||||
"""后台用户。"""
|
||||
"""后台用户。
|
||||
|
||||
权限模型:
|
||||
- is_admin=True:管理员,拥有全部权限,不受 perms 限制
|
||||
- is_admin=False:按 perms 授权(JSON 数组,如 ["tasks","devices"])
|
||||
- 权限位:tasks=任务管理, devices=设备控制, apks=应用管理, logs=日志查看
|
||||
- 用户管理本身只有管理员能做(web_server 层强制),不设普通权限位
|
||||
"""
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
username = db.Column(db.String(80), unique=True, nullable=False)
|
||||
password_hash = db.Column(db.String(255), nullable=False)
|
||||
is_admin = db.Column(db.Boolean, default=True)
|
||||
perms = db.Column(db.Text, default="[]") # JSON 数组:业务权限位
|
||||
|
||||
def get_perms(self):
|
||||
try:
|
||||
return json.loads(self.perms or "[]")
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
def set_perms(self, lst):
|
||||
"""设置权限位。管理员强制为全部权限(内部不区分)。"""
|
||||
self.perms = json.dumps(list(lst or []), ensure_ascii=False)
|
||||
|
||||
def has_perm(self, perm):
|
||||
"""是否拥有指定权限。管理员恒为 True。"""
|
||||
if self.is_admin:
|
||||
return True
|
||||
return perm in self.get_perms()
|
||||
|
||||
def set_password(self, password):
|
||||
self.password_hash = generate_password_hash(password)
|
||||
@@ -195,7 +219,8 @@ class ApkFile(db.Model):
|
||||
# 版本化 schema 迁移:新增结构变更时在此追加 (版本号, 说明, SQL)
|
||||
# 版本号单调递增,只执行比当前 schema_version 新的迁移。
|
||||
SCHEMA_MIGRATIONS = [
|
||||
# (1, "初始 schema(由 create_all 建立,版本标记从 1 开始)", None),
|
||||
(1, "用户权限位:user 表新增 perms 列(JSON 数组,默认空=无业务权限,管理员不受限)",
|
||||
"ALTER TABLE user ADD COLUMN perms TEXT DEFAULT '[]'"),
|
||||
]
|
||||
|
||||
|
||||
|
||||
+60
-9
@@ -20,6 +20,29 @@ function _csrfHeaders(){
|
||||
return _csrfToken?{'X-CSRF-Token':_csrfToken}:{};
|
||||
}
|
||||
|
||||
// ===== 当前用户与权限 =====
|
||||
// _me 来自 /api/me:{username, is_admin, perms:[tasks/devices/apks/logs]}
|
||||
// 管理员 perms 恒为全部。无权限的功能入口(tab/按钮)用 data-perm 标记或 _can() 判断隐藏。
|
||||
let _me={is_admin:false,perms:[],username:''};
|
||||
async function loadMe(){
|
||||
try{
|
||||
const r=await apiGet('/api/me');
|
||||
if(r&&r.ok)_me=r.user||{is_admin:false,perms:[],username:''};
|
||||
const un=document.getElementById('me-username');
|
||||
if(un)un.textContent=_me.username||'';
|
||||
// 隐藏无权限的 tab 和按钮(data-perm="admin" 表示仅管理员)
|
||||
document.querySelectorAll('[data-perm]').forEach(function(el){
|
||||
const p=el.dataset.perm;
|
||||
const ok=(p==='admin')?_me.is_admin:(_me.perms||[]).includes(p);
|
||||
if(!ok)el.style.display='none';
|
||||
});
|
||||
}catch(e){}
|
||||
}
|
||||
function _can(perm){
|
||||
return _me.is_admin||(_me.perms||[]).includes(perm);
|
||||
}
|
||||
const PERM_LABELS={tasks:'任务管理',devices:'设备控制',apks:'应用管理',logs:'日志查看'};
|
||||
|
||||
async function apiGet(url){
|
||||
const r=await fetch(url);
|
||||
if(r.status===401){window.location='/login';return null;}
|
||||
@@ -1892,8 +1915,10 @@ function renderActionRow(a){
|
||||
'<td>'+n+'</td>'+
|
||||
'<td style="font-size:12px;color:#94a3b8">'+esc(a.created_at||'-')+'</td>'+
|
||||
'<td>'+
|
||||
'<button class="btn btn-xs" onclick="openActionModal(\''+a.id+'\')">编辑</button> '+
|
||||
'<button class="btn btn-danger btn-xs" onclick="deleteAction(\''+a.id+'\')">删除</button>'+
|
||||
(_can('tasks')?(
|
||||
'<button class="btn btn-xs" onclick="openActionModal(\''+a.id+'\')">编辑</button> '+
|
||||
'<button class="btn btn-danger btn-xs" onclick="deleteAction(\''+a.id+'\')">删除</button>'
|
||||
):'<span class="text-muted">只读</span>')+
|
||||
'</td>'+
|
||||
'</tr>';
|
||||
}
|
||||
@@ -1976,8 +2001,10 @@ function renderGroupRow(g){
|
||||
'<td>'+serials.length+'</td>'+
|
||||
'<td class="text-muted" style="max-width:300px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">'+(serials.length?serials.map(esc).join(', '):'-')+'</td>'+
|
||||
'<td>'+
|
||||
'<button class="btn btn-xs" onclick="openGroupModal(\''+esc(g.name)+'\')">编辑</button>'+
|
||||
'<button class="btn btn-danger btn-xs" onclick="deleteGroup(\''+esc(g.name)+'\')">删除</button>'+
|
||||
(_can('tasks')?(
|
||||
'<button class="btn btn-xs" onclick="openGroupModal(\''+esc(g.name)+'\')">编辑</button>'+
|
||||
'<button class="btn btn-danger btn-xs" onclick="deleteGroup(\''+esc(g.name)+'\')">删除</button>'
|
||||
):'<span class="text-muted">只读</span>')+
|
||||
'</td>'+
|
||||
'</tr>';
|
||||
}
|
||||
@@ -2086,13 +2113,29 @@ function renderUserRow(u){
|
||||
'</tr>';
|
||||
}
|
||||
|
||||
function togglePermBoxes(){
|
||||
// 勾选"管理员权限"时:权限位全选并禁用(管理员拥有全部权限)
|
||||
const isAdmin=document.getElementById('u-is_admin').checked;
|
||||
document.querySelectorAll('.u-perm').forEach(cb=>{
|
||||
cb.disabled=isAdmin;
|
||||
if(isAdmin)cb.checked=true;
|
||||
});
|
||||
}
|
||||
|
||||
function openUserModal(uid){
|
||||
const isEdit=!!uid;
|
||||
document.getElementById('modal-title').textContent=isEdit?'编辑用户':'新建用户';
|
||||
const permBoxes=Object.keys(PERM_LABELS).map(p=>
|
||||
'<label style="margin-right:12px"><input type="checkbox" class="u-perm" value="'+p+'">'+PERM_LABELS[p]+'</label>'
|
||||
).join('');
|
||||
document.getElementById('modal-body').innerHTML=
|
||||
'<div class="form-group"><label>用户名</label><input type="text" class="form-control" id="u-username" '+(isEdit?'disabled':'')+' placeholder="用户名"></div>'+
|
||||
'<div class="form-group"><label>密码'+(isEdit?'(留空不修改)':'')+'</label><input type="password" class="form-control" id="u-password" placeholder="密码"></div>'+
|
||||
'<div class="form-group form-check"><input type="checkbox" id="u-is_admin" checked><label for="u-is_admin">管理员权限</label></div>';
|
||||
'<div class="form-group form-check"><input type="checkbox" id="u-is_admin" onchange="togglePermBoxes()" checked><label for="u-is_admin">管理员权限(拥有全部权限)</label></div>'+
|
||||
'<div class="form-group"><label>业务权限(管理员自动拥有全部,无需勾选)</label>'+
|
||||
'<div>'+permBoxes+'</div>'+
|
||||
'<div class="help">任务管理=任务/自定义动作/分组;设备控制=停止/释放/清异常/扫描/抓元素;应用管理=APK上传安装;日志查看=日志页</div>'+
|
||||
'</div>';
|
||||
|
||||
document.getElementById('modal-footer').innerHTML=
|
||||
'<button class="btn" onclick="closeModal()">取消</button>'+
|
||||
@@ -2100,13 +2143,19 @@ function openUserModal(uid){
|
||||
|
||||
if(isEdit){
|
||||
// 预填充(从表格数据获取)
|
||||
fetch('/api/users').then(r=>r.json()).then(r=>{
|
||||
const u=r.users.find(x=>x.id===uid);
|
||||
apiGet('/api/users').then(r=>{
|
||||
const u=(r&&r.users||[]).find(x=>x.id===uid);
|
||||
if(u){
|
||||
document.getElementById('u-username').value=u.username;
|
||||
document.getElementById('u-is_admin').checked=u.is_admin;
|
||||
document.querySelectorAll('.u-perm').forEach(cb=>{
|
||||
cb.checked=(u.perms||[]).includes(cb.value);
|
||||
});
|
||||
togglePermBoxes();
|
||||
}
|
||||
});
|
||||
}else{
|
||||
togglePermBoxes();
|
||||
}
|
||||
showModal();
|
||||
}
|
||||
@@ -2115,15 +2164,16 @@ async function saveUser(uid){
|
||||
const username=document.getElementById('u-username').value.trim();
|
||||
const password=document.getElementById('u-password').value;
|
||||
const is_admin=document.getElementById('u-is_admin').checked;
|
||||
const perms=[...document.querySelectorAll('.u-perm:checked')].map(cb=>cb.value);
|
||||
|
||||
if(uid){
|
||||
const data={is_admin:is_admin};
|
||||
const data={is_admin:is_admin,perms:perms};
|
||||
if(password)data.password=password;
|
||||
const r=await apiPut('/api/users/'+uid,data);
|
||||
handleResult(r,()=>{closeModal();loadUsers();});
|
||||
}else{
|
||||
if(!username||!password){showToast('用户名和密码不能为空','error');return;}
|
||||
const r=await apiPost('/api/users',{username:username,password:password,is_admin:is_admin});
|
||||
const r=await apiPost('/api/users',{username:username,password:password,is_admin:is_admin,perms:perms});
|
||||
handleResult(r,()=>{closeModal();loadUsers();});
|
||||
}
|
||||
}
|
||||
@@ -2520,4 +2570,5 @@ async function scanForeground(){
|
||||
|
||||
// ================== 初始化 ==================
|
||||
initCsrf(); // 获取 CSRF token(非 GET 请求需携带)
|
||||
loadMe(); // 获取当前用户权限,隐藏无权限的功能入口
|
||||
showTab('monitor');
|
||||
|
||||
@@ -354,7 +354,7 @@ select.form-control{cursor:pointer}
|
||||
<div class="page-title" style="font-size:16px">自定义动作管理</div>
|
||||
<div class="page-sub">把常用步骤打包成可复用的动作,拖入任务步骤编辑器使用;也可在任务编辑器里"☑选中步骤→打包"生成</div>
|
||||
<div class="toolbar">
|
||||
<button class="btn btn-primary" onclick="openActionModal()">新建动作</button>
|
||||
<button class="btn btn-primary" onclick="openActionModal()" data-perm="tasks">新建动作</button>
|
||||
<button class="btn" onclick="loadCustomActions()">刷新</button>
|
||||
<input class="form-control list-search" id="search-actions" placeholder="搜索动作名...">
|
||||
</div>
|
||||
@@ -418,7 +418,7 @@ select.form-control{cursor:pointer}
|
||||
<div class="page-title">设备分组管理</div>
|
||||
<div class="page-sub">管理设备分组,用于任务按分组执行</div>
|
||||
<div class="toolbar">
|
||||
<button class="btn btn-primary" onclick="openGroupModal()">新建分组</button>
|
||||
<button class="btn btn-primary" onclick="openGroupModal()" data-perm="tasks">新建分组</button>
|
||||
<button class="btn" onclick="loadGroups()">刷新</button>
|
||||
<input class="form-control list-search" id="search-groups" placeholder="搜索分组名/描述...">
|
||||
</div>
|
||||
|
||||
+103
-22
@@ -93,6 +93,77 @@ def api_csrf():
|
||||
return jsonify({"ok": True, "token": _csrf_token()})
|
||||
|
||||
|
||||
# ================== 权限控制 ==================
|
||||
# 业务权限位(is_admin 管理员拥有全部权限,不受 perms 限制):
|
||||
# tasks — 任务管理:任务/自定义动作/分组的创建、修改、删除、启停、执行
|
||||
# devices — 设备控制:停止设备、释放占用、清除异常、前台扫描、元素抓取
|
||||
# apks — 应用管理:APK 上传、安装、删除
|
||||
# logs — 日志查看
|
||||
# 查看类接口(GET 状态/列表)所有登录用户可用;用户管理仅管理员可用(admin_required)。
|
||||
PERM_TASKS = "tasks"
|
||||
PERM_DEVICES = "devices"
|
||||
PERM_APKS = "apks"
|
||||
PERM_LOGS = "logs"
|
||||
ALL_PERMS = (PERM_TASKS, PERM_DEVICES, PERM_APKS, PERM_LOGS)
|
||||
_PERM_LABELS = {PERM_TASKS: "任务管理", PERM_DEVICES: "设备控制",
|
||||
PERM_APKS: "应用管理", PERM_LOGS: "日志查看"}
|
||||
|
||||
|
||||
def _has_perm(perm):
|
||||
"""当前用户是否拥有指定权限。管理员恒为 True。"""
|
||||
u = current_user
|
||||
return bool(u and (u.is_admin or u.has_perm(perm)))
|
||||
|
||||
|
||||
def _validate_perms(raw):
|
||||
"""校验权限位列表:只保留合法值、去重。返回合法列表。"""
|
||||
valid = set(ALL_PERMS)
|
||||
out = []
|
||||
for p in raw or []:
|
||||
if p in valid and p not in out:
|
||||
out.append(p)
|
||||
return out
|
||||
|
||||
|
||||
def perm_required(perm):
|
||||
"""路由装饰器:要求登录且拥有指定业务权限,否则 403。"""
|
||||
def deco(fn):
|
||||
@functools.wraps(fn)
|
||||
@login_required
|
||||
def wrapper(*args, **kwargs):
|
||||
if not _has_perm(perm):
|
||||
return jsonify({"ok": False,
|
||||
"error": f"无权限执行此操作(需要权限: {_PERM_LABELS.get(perm, perm)})"}), 403
|
||||
return fn(*args, **kwargs)
|
||||
return wrapper
|
||||
return deco
|
||||
|
||||
|
||||
def admin_required(fn):
|
||||
"""路由装饰器:仅管理员可用(用户管理类接口),否则 403。"""
|
||||
@functools.wraps(fn)
|
||||
@login_required
|
||||
def wrapper(*args, **kwargs):
|
||||
if not current_user.is_admin:
|
||||
return jsonify({"ok": False, "error": "仅管理员可执行此操作"}), 403
|
||||
return fn(*args, **kwargs)
|
||||
return wrapper
|
||||
|
||||
|
||||
@app.route("/api/me")
|
||||
@login_required
|
||||
def api_me():
|
||||
"""当前登录用户信息(含权限位),前端据此隐藏无权限的功能入口。"""
|
||||
u = current_user
|
||||
return jsonify({"ok": True, "user": {
|
||||
"id": u.id,
|
||||
"username": u.username,
|
||||
"is_admin": bool(u.is_admin),
|
||||
# 管理员返回全部权限位,前端统一用 perms 判断
|
||||
"perms": list(ALL_PERMS) if u.is_admin else u.get_perms(),
|
||||
}})
|
||||
|
||||
|
||||
# ================== 页面路由 ==================
|
||||
@app.route("/")
|
||||
@login_required
|
||||
@@ -209,7 +280,7 @@ def api_status():
|
||||
|
||||
|
||||
@app.route("/api/scan_foreground", methods=["POST"])
|
||||
@login_required
|
||||
@perm_required(PERM_DEVICES)
|
||||
def api_scan_foreground():
|
||||
"""手动触发前台 App 扫描(不打扰设备)。"""
|
||||
started = mgr._fg_scanner.scan_once()
|
||||
@@ -315,13 +386,13 @@ def api_jobs_delete(job_id):
|
||||
|
||||
|
||||
@app.route("/api/jobs/<job_id>/run", methods=["POST"])
|
||||
@login_required
|
||||
@perm_required(PERM_TASKS)
|
||||
def api_jobs_run(job_id):
|
||||
return jsonify(mgr.run_job_now(job_id))
|
||||
|
||||
|
||||
@app.route("/api/jobs/<job_id>/toggle", methods=["POST"])
|
||||
@login_required
|
||||
@perm_required(PERM_TASKS)
|
||||
def api_jobs_toggle(job_id):
|
||||
enabled = (request.json or {}).get("enabled", True)
|
||||
job = mgr.toggle_job(job_id, enabled)
|
||||
@@ -338,7 +409,7 @@ def api_groups_list():
|
||||
|
||||
|
||||
@app.route("/api/groups", methods=["POST"])
|
||||
@login_required
|
||||
@perm_required(PERM_TASKS)
|
||||
def api_groups_create():
|
||||
data = request.json or {}
|
||||
name = (data.get("name") or "").strip()
|
||||
@@ -351,7 +422,7 @@ def api_groups_create():
|
||||
|
||||
|
||||
@app.route("/api/groups/<name>", methods=["PUT"])
|
||||
@login_required
|
||||
@perm_required(PERM_TASKS)
|
||||
def api_groups_update(name):
|
||||
if name not in mgr.groups:
|
||||
return jsonify({"ok": False, "error": "分组不存在"}), 404
|
||||
@@ -372,14 +443,15 @@ def api_groups_delete(name):
|
||||
|
||||
# ================== API:用户管理 CRUD ==================
|
||||
@app.route("/api/users")
|
||||
@login_required
|
||||
@admin_required
|
||||
def api_users_list():
|
||||
users = [{"id": u.id, "username": u.username, "is_admin": u.is_admin} for u in User.query.all()]
|
||||
users = [{"id": u.id, "username": u.username,
|
||||
"is_admin": u.is_admin, "perms": u.get_perms()} for u in User.query.all()]
|
||||
return jsonify({"ok": True, "users": users})
|
||||
|
||||
|
||||
@app.route("/api/users", methods=["POST"])
|
||||
@login_required
|
||||
@admin_required
|
||||
def api_users_create():
|
||||
data = request.json or {}
|
||||
username = (data.get("username") or "").strip()
|
||||
@@ -388,16 +460,18 @@ def api_users_create():
|
||||
return jsonify({"ok": False, "error": "用户名和密码不能为空"}), 400
|
||||
if User.query.filter_by(username=username).first():
|
||||
return jsonify({"ok": False, "error": "用户名已存在"}), 400
|
||||
u = User(username=username, is_admin=data.get("is_admin", True))
|
||||
# 默认普通用户(最小权限);管理员由 is_admin 决定,权限位照常保存(降级后生效)
|
||||
u = User(username=username, is_admin=bool(data.get("is_admin", False)))
|
||||
u.set_perms(_validate_perms(data.get("perms") or []))
|
||||
u.set_password(password)
|
||||
db.session.add(u)
|
||||
db.session.commit()
|
||||
_log.info(f"创建用户 {username}")
|
||||
_log.info(f"创建用户 {username} (admin={u.is_admin}, perms={u.get_perms()})")
|
||||
return jsonify({"ok": True, "msg": "用户已创建"})
|
||||
|
||||
|
||||
@app.route("/api/users/<int:uid>", methods=["PUT"])
|
||||
@login_required
|
||||
@admin_required
|
||||
def api_users_update(uid):
|
||||
u = User.query.get(uid)
|
||||
if not u:
|
||||
@@ -406,14 +480,19 @@ def api_users_update(uid):
|
||||
if "password" in data and data["password"]:
|
||||
u.set_password(data["password"])
|
||||
if "is_admin" in data:
|
||||
u.is_admin = bool(data["is_admin"])
|
||||
new_admin = bool(data["is_admin"])
|
||||
if u.is_admin and not new_admin and User.query.filter_by(is_admin=True).count() <= 1:
|
||||
return jsonify({"ok": False, "error": "不能取消最后一个管理员"}), 400
|
||||
u.is_admin = new_admin
|
||||
if "perms" in data:
|
||||
u.set_perms(_validate_perms(data["perms"]))
|
||||
db.session.commit()
|
||||
_log.info(f"更新用户 {u.username}")
|
||||
_log.info(f"更新用户 {u.username} (admin={u.is_admin}, perms={u.get_perms()})")
|
||||
return jsonify({"ok": True, "msg": "用户已更新"})
|
||||
|
||||
|
||||
@app.route("/api/users/<int:uid>", methods=["DELETE"])
|
||||
@login_required
|
||||
@admin_required
|
||||
def api_users_delete(uid):
|
||||
u = User.query.get(uid)
|
||||
if not u:
|
||||
@@ -422,6 +501,8 @@ def api_users_delete(uid):
|
||||
return jsonify({"ok": False, "error": "不能删除默认管理员"}), 400
|
||||
if u.id == current_user.id:
|
||||
return jsonify({"ok": False, "error": "不能删除当前登录用户"}), 400
|
||||
if u.is_admin and User.query.filter_by(is_admin=True).count() <= 1:
|
||||
return jsonify({"ok": False, "error": "不能删除最后一个管理员"}), 400
|
||||
db.session.delete(u)
|
||||
db.session.commit()
|
||||
_log.info(f"删除用户 {u.username}")
|
||||
@@ -430,7 +511,7 @@ def api_users_delete(uid):
|
||||
|
||||
# ================== API:日志查看 ==================
|
||||
@app.route("/api/logs")
|
||||
@login_required
|
||||
@perm_required(PERM_LOGS)
|
||||
def api_logs():
|
||||
files = list(_MODULE_FILES.values())
|
||||
current = request.args.get("file", "core.log")
|
||||
@@ -448,7 +529,7 @@ def api_logs():
|
||||
|
||||
# ================== API:运行控制 ==================
|
||||
@app.route("/api/stop_device", methods=["POST"])
|
||||
@login_required
|
||||
@perm_required(PERM_DEVICES)
|
||||
def api_stop_device():
|
||||
serial = (request.json or {}).get("serial", "")
|
||||
if mgr.stop_device(serial):
|
||||
@@ -535,7 +616,7 @@ def api_custom_actions_list():
|
||||
|
||||
|
||||
@app.route("/api/custom_actions", methods=["POST"])
|
||||
@login_required
|
||||
@perm_required(PERM_TASKS)
|
||||
def api_custom_actions_create():
|
||||
data = request.json or {}
|
||||
name = (data.get("name") or "").strip()
|
||||
@@ -554,7 +635,7 @@ def api_custom_actions_create():
|
||||
|
||||
|
||||
@app.route("/api/custom_actions/<action_id>", methods=["PUT"])
|
||||
@login_required
|
||||
@perm_required(PERM_TASKS)
|
||||
def api_custom_actions_update(action_id):
|
||||
row = CustomAction.query.get(action_id)
|
||||
if not row:
|
||||
@@ -576,7 +657,7 @@ def api_custom_actions_update(action_id):
|
||||
|
||||
|
||||
@app.route("/api/custom_actions/<action_id>", methods=["DELETE"])
|
||||
@login_required
|
||||
@perm_required(PERM_TASKS)
|
||||
def api_custom_actions_delete(action_id):
|
||||
row = CustomAction.query.get(action_id)
|
||||
if not row:
|
||||
@@ -587,7 +668,7 @@ def api_custom_actions_delete(action_id):
|
||||
|
||||
|
||||
@app.route("/api/uiauto/elements")
|
||||
@login_required
|
||||
@perm_required(PERM_DEVICES)
|
||||
def api_uiauto_elements():
|
||||
"""获取设备当前 UI 元素树(供步骤编辑器"抓取元素"用)。
|
||||
|
||||
@@ -705,7 +786,7 @@ def api_apks_list():
|
||||
|
||||
|
||||
@app.route("/api/apks/upload", methods=["POST"])
|
||||
@login_required
|
||||
@perm_required(PERM_APKS)
|
||||
def api_apks_upload():
|
||||
"""上传 APK 文件(multipart/form-data,字段名 file)。"""
|
||||
file = request.files.get("file")
|
||||
@@ -719,7 +800,7 @@ def api_apks_upload():
|
||||
|
||||
|
||||
@app.route("/api/apks/<apk_id>", methods=["DELETE"])
|
||||
@login_required
|
||||
@perm_required(PERM_APKS)
|
||||
def api_apks_delete(apk_id):
|
||||
"""删除 APK 文件和记录。"""
|
||||
ok, msg = apk_mgr.delete(apk_id)
|
||||
|
||||
Reference in New Issue
Block a user