文件
wangchuanli f36149efc3 feat(安全): 对外暴露面加固 + 界面去 AI 化(v1.5.0)
界面(去 AI 味):
- 大屏页清除 114 处生成器残留属性 data-page-node-id
- 视觉系统改回工程控制台风格:去 radial/linear-gradient、去辉光、
  去标题前彩色装饰条,改为中性灰阶 + 单一蓝色强调色;KPI 色条改状态点
- 精简各页说教式长提示;修掉 profile.html 泄漏到页面上的 Markdown 星号
- 删除登录页过时的「默认账号 admin / admin123」提示(1.4.0 起已无默认口令)

安全与隐私(按「将会被公网访问」收口):
- 内部异常只回 8 位事件号,完整堆栈进服务端日志(web/api.py::_internal)
- 导出文件名收敛:防响应头注入与路径穿越;manage.py passwd 补用户名校验
- 登录对不存在的账号也走一次哑哈希,抹平用户名枚举的时序差异
- /api/* 读接口限速 240 次 / 60 秒 / 账号(挡住循环调 /api/bundle)
- 进程 umask 0077 + 目录 0700 / 文件 0600:对话正文与主密钥的落盘权限
- 表名与库文件路径只对管理员下发;大屏页所有数据插值转义
- --debug 只允许绑定回环地址;新增 Permissions-Policy 与 413 处理器

文档:
- DEPLOYMENT 新增第十三节「安全与隐私基线」;迁移表补 1.4.0 → 1.5.0 行
- SECURITY 更新支持范围、新增「信息泄漏收敛」小节与上线检查项
- .codebuddy/ 加入 .gitignore(助手工作记忆不进仓库)

版本:1.4.0 → 1.5.0(无库结构变更,user_version 仍为 4)
验证:python tools/smoke.py → ok=264 fail=0;python tools/check_docs.py → 0 处问题
2026-09-18 11:13:17 +08:00

120 行
5.5 KiB
HTML

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
{% extends "base.html" %}
{% block title %}个人中心 · {{ project_title }}{% endblock %}
{% block body %}
<div class="pagehead">
<div>
<h1>个人中心</h1>
<p class="lead">账号 <b>{{ me.username }}</b> · 注册于 {{ (me.created_at or '')[:16] }} ·
最近登录 {{ (me.last_login_at or '未登录')[:19] }}</p>
</div>
<div class="actions">
<a class="btn" href="{{ url_for('views.config_page') }}">管理我的凭证</a>
<a class="btn ghost" href="{{ url_for('views.profile_export') }}"
title="导出你的全部用量明细、采集历史、操作审计与有效配置(不含 Cookie 明文)">导出我的全部数据</a>
</div>
</div>
<div class="kpis">
<div class="kpi" style="--c:var(--cyan)">
<span>我的记录</span><b>{{ '{:,}'.format(my.n or 0) }}</b>
<i>{{ my.d0 or '—' }} ~ {{ my.d1 or '—' }}</i>
</div>
<div class="kpi" style="--c:var(--violet)">
<span>我的积分</span><b>{{ '%.2f'|format(my.c or 0) }}</b>
<i>仅统计归属本账号的数据</i>
</div>
<div class="kpi" style="--c:var(--blue)">
<span>采集次数</span><b>{{ '{:,}'.format(runs) }}</b>
<i>{% if sch.last %}最近 {{ sch.last.started_at[5:16] if sch.last.started_at else '—' }}{% else %}尚无采集{% endif %}</i>
</div>
<div class="kpi" style="--c:{{ 'var(--green)' if cred.set and not cred.broken else 'var(--red)' }}">
<span>我的 Cookie</span>
<b>{% if cred.broken %}无法解密{% elif cred.set %}已配置{% else %}未配置{% endif %}</b>
<i>{% if cred.set and not cred.broken %}{{ cred.chars }} 字符,结尾 …{{ cred.tail }}
{%- elif cred.broken %}实例密钥被更换,请重新粘贴
{%- else %}采集需要本人凭证{% endif %}</i>
</div>
</div>
<div class="grid2">
<section class="card">
<h2>修改资料</h2>
<form id="formProfile">
<label class="row"><span>用户名</span>
<input value="{{ me.username }}" disabled spellcheck="false">
<em class="unit">登录名不可改</em></label>
<label class="row"><span>显示名</span>
<input name="display_name" maxlength="64" value="{{ me.display_name or '' }}" spellcheck="false"></label>
<label class="row"><span>邮箱</span>
<input name="email" type="email" maxlength="128" value="{{ me.email or '' }}" spellcheck="false"></label>
<button class="btn primary" type="submit">保存资料</button>
</form>
</section>
<section class="card">
<h2>修改登录密码</h2>
<form id="formPwd">
<label class="col">原密码<input name="old" type="password" autocomplete="current-password"></label>
<label class="col">新密码<input name="new" type="password" autocomplete="new-password"></label>
<label class="col">确认新密码<input name="new2" type="password" autocomplete="new-password"></label>
<button class="btn primary" type="submit">修改密码</button>
<p class="hint">至少 {{ pwd_min }} 位,需包含大写字母、小写字母、数字、符号中的至少两类。
修改成功后其他设备上的登录会立刻失效,本机会话保留。</p>
</form>
</section>
</div>
<section class="card">
<div class="cardhead">
<h2>导出我的全部数据</h2>
<span class="tag mute">数据可携带</span>
</div>
<p class="hint" style="margin-top:0">
下载本账号的用量明细、采集历史、操作审计与有效配置。
不含 Cookie 明文,也不含其他账号的数据与实例级运行日志。
</p>
<div class="btnrow">
<a class="btn primary" href="{{ url_for('views.profile_export') }}">导出我的全部数据(zip)</a>
<a class="btn ghost" href="{{ url_for('views.records') }}">只导出用量明细(CSV)</a>
</div>
<p class="hint">两次导出之间至少间隔 10 秒。</p>
</section>
<section class="card">
<div class="cardhead">
<h2>我的采集凭证</h2>
<span class="tag {{ 'ok' if cred.set and not cred.broken else ('bad' if not cred.set else 'warn') }}">
{{ '正常' if cred.set and not cred.broken else ('未配置' if not cred.set else '需要重配') }}</span>
</div>
<table class="kv">
<tr><th>状态</th><td>
{% if cred.broken %}<span class="tag bad">已保存但无法解密</span>,请到「配置管理」重新粘贴
{% elif cred.set %}<span class="tag ok">已保存(密文入库)</span>
{% else %}<span class="tag bad">未配置</span>{% endif %}
</td></tr>
<tr><th>字符数 / 尾部</th><td class="mono">{{ cred.chars or 0 }} / {{ ('…' + cred.tail) if cred.tail else '—' }}</td></tr>
<tr><th>最后更新</th><td class="mono">{{ cred.at or '—' }}</td></tr>
<tr><th>调度</th><td>
{% if sch.enabled %}{{ sch.times | join(' · ') or '未设置时刻' }}{% else %}<span class="tag bad">已停用</span>{% endif %}
{% if sch.next_run %}· 下次 <span class="mono">{{ sch.next_run }}</span>{% endif %}
</td></tr>
</table>
<p class="hint">
凭证以密文入库(主密钥在 <code>data/instance.json</code>),页面与接口都不回传明文。
更换请到<a href="{{ url_for('views.config_page') }}">配置管理</a>重新粘贴 Cookie 与 User-Agent。
</p>
</section>
{% endblock %}
{% block scripts %}
<script src="{{ url_for('static', filename='js/app.js') }}"></script>
<script>
WBU.bindForm('#formProfile', '/api/profile');
WBU.bindForm('#formPwd', '/api/password', {
validate: function (d) { return d.new === d.new2 ? null : '两次输入的新密码不一致'; }
});
</script>
{% endblock %}