This commit is contained in:
Marsway 2026-01-13 02:22:47 +08:00
parent 86e689f453
commit 96ef2cf88c
2 changed files with 75 additions and 9 deletions

View File

@ -0,0 +1,33 @@
{% extends "sqladmin/layout.html" %}
{% from 'sqladmin/_macros.html' import render_form_fields %}
{% block content %}
### 编辑{{ model_view.name }}
{% if error %}
{{ error }}
{% endif %}
<form method="post">
{{ render_form_fields(form, form_opts=form_opts) }}
<div class="row mb-3">
<label class="form-label col-sm-2 col-form-label">密文配置secret_cfg</label>
<div class="col-sm-10">
<textarea name="secret_cfg" class="form-control" rows="8" placeholder='留空表示不修改;填写将覆盖并加密保存。示例:{"token":"xxx"}'></textarea>
<div class="form-text">
出于安全考虑,编辑页不回显历史密文。留空表示不修改;填写 JSON 对象将覆盖原值并重新加密保存。
</div>
</div>
</div>
<div class="mt-3 d-flex gap-2">
<button type="submit" class="btn btn-primary">保存</button>
<a class="btn btn-secondary" href="{{ url_for('admin:list', identity=model_view.identity) }}">取消</a>
</div>
</form>
{% endblock %}

View File

@ -69,9 +69,15 @@ class JobAdmin(ModelView, model=Job):
# 为 Job 详情页指定模板(用于调整按钮间距) # 为 Job 详情页指定模板(用于调整按钮间距)
details_template = "job_details.html" details_template = "job_details.html"
# 编辑页secret_cfg 只写不读(不回显密文;留空表示不更新)
edit_template = "job_edit.html"
# 列表页模板:加入每行 Run Now # 列表页模板:加入每行 Run Now
list_template = "job_list.html" list_template = "job_list.html"
# 编辑页排除 secret_cfg避免回显密文由自定义模板额外渲染一个空输入框
form_edit_rules = [Job.id, Job.enabled, Job.cron_expr, Job.handler_path, Job.public_cfg]
column_labels = { column_labels = {
"id": "任务ID", "id": "任务ID",
"enabled": "启用", "enabled": "启用",
@ -141,16 +147,38 @@ class JobAdmin(ModelView, model=Job):
raise ValueError("public_cfg must be a JSON object") raise ValueError("public_cfg must be a JSON object")
data["public_cfg"] = pcfg data["public_cfg"] = pcfg
# secret_cfg必须是合法 JSON 对象dict并且保存时必须加密落库 # secret_cfg
scfg = data.get("secret_cfg") # - 创建:必须是合法 JSON 对象dict并且保存时必须加密落库
if isinstance(scfg, str): # - 编辑:出于安全考虑不回显密文;若留空则保留原密文不更新;若填写则按 JSON 校验并加密覆盖
if is_created:
scfg = data.get("secret_cfg")
if isinstance(scfg, str):
try:
scfg = json.loads(scfg)
except json.JSONDecodeError as e:
raise ValueError("secret_cfg must be a JSON object") from e
if not isinstance(scfg, dict):
raise ValueError("secret_cfg must be a JSON object")
data["secret_cfg"] = encrypt_json(scfg)
else:
# 自定义编辑页会以 textarea 传回 secret_cfg可能不存在或为空
try: try:
scfg = json.loads(scfg) form = await request.form()
except json.JSONDecodeError as e: raw = form.get("secret_cfg")
raise ValueError("secret_cfg must be a JSON object") from e except Exception:
if not isinstance(scfg, dict): raw = None
raise ValueError("secret_cfg must be a JSON object") raw_s = str(raw).strip() if raw is not None else ""
data["secret_cfg"] = encrypt_json(scfg) if not raw_s:
# 留空:不更新密文字段
data.pop("secret_cfg", None)
else:
try:
scfg2 = json.loads(raw_s)
except json.JSONDecodeError as e:
raise ValueError("secret_cfg must be a JSON object") from e
if not isinstance(scfg2, dict):
raise ValueError("secret_cfg must be a JSON object")
data["secret_cfg"] = encrypt_json(scfg2)
class JobLogAdmin(ModelView, model=JobLog): class JobLogAdmin(ModelView, model=JobLog):
@ -208,6 +236,11 @@ class JobLogAdmin(ModelView, model=JobLog):
column_formatters_detail = { column_formatters_detail = {
JobLog.started_at: lambda m, a: _fmt_dt_seconds(m.started_at), JobLog.started_at: lambda m, a: _fmt_dt_seconds(m.started_at),
JobLog.finished_at: lambda m, a: _fmt_dt_seconds(m.finished_at), JobLog.finished_at: lambda m, a: _fmt_dt_seconds(m.finished_at),
JobLog.message: lambda m, a: Markup(
"<pre style='max-height:240px;overflow:auto;white-space:pre-wrap'>"
+ (m.message or "")
+ "</pre>"
),
JobLog.traceback: lambda m, a: Markup(f"<pre style='white-space:pre-wrap'>{m.traceback or ''}</pre>"), JobLog.traceback: lambda m, a: Markup(f"<pre style='white-space:pre-wrap'>{m.traceback or ''}</pre>"),
JobLog.run_log: lambda m, a: Markup( JobLog.run_log: lambda m, a: Markup(
"<pre style='max-height:480px;overflow:auto;white-space:pre-wrap'>" "<pre style='max-height:480px;overflow:auto;white-space:pre-wrap'>"