- 新增备份管理页与 API:在线快照、自动周期备份、按份数清理、下载、一键恢复(恢复前自动兜底) - 新增 /profile/export,普通用户可导出本人全部数据(不含 Cookie 明文) - 修复 X-Forwarded-For 可伪造导致三道 IP 防线失效,统一走 client_ip() 取客户端地址 - 取消 admin123 硬编码默认口令,留空则生成随机初始口令并仅打印一次 - .dockerignore 排除 backups/ 并加构建期断言,防止密钥随镜像分发 - 新增会话版本号,改密/停用/删除及恢复备份后其他会话立即失效 - 新增容器资源上限、采集跨度硬顶 31 天、重操作最小间隔与并发 409 - 新增访问日志、HSTS 条件下发、口令黑名单、验证码抗模板匹配、instance.json 0600 - 版本号升至 1.4.0,同步更新 README、SECURITY、.env.example 与 compose 配置
126 行
6.0 KiB
HTML
126 行
6.0 KiB
HTML
{% 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 }} 位,且需包含大写字母、小写字母、数字、符号中的至少两类,
|
||
且不能是常见弱口令。
|
||
<b>修改成功后其他设备上的登录会立刻失效</b>(本机这次会话保留,不必重新登录)——
|
||
「怀疑被盗所以改密码」才能真正生效。</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">
|
||
打包下载<b>属于你账号</b>的所有内容:用量明细、采集历史、与你相关的操作审计、
|
||
以及账号信息与对你有效的配置。
|
||
<br><b>不含</b> 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>),
|
||
页面与接口**任何时候都不回传明文**,只显示长度与尾部 4 位。要更换请到
|
||
<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 %}
|