feat(install): show the sync token on the install page
插件必须填 NAVI_SYNC_TOKEN,但页面上没有任何地方能看到它 —— 用户只能去翻 .env,实际就是配不起来。 - admin-server 新增 GET /admin/api/sync-info,返回 syncToken / hasToken。 放在 /admin/ 下而不是 /api/ 下是必须的:插件上报用的 /api/history/sync 被门户 auth_request 放行,若令牌挂在同一前缀下就等于公开。 实测 /api/history/sync-info 返回 401,只有 /admin/api/sync-info 能取到。 - 安装引导页把「第一步」改为填写两项(服务地址 + 同步令牌),各自可复制; 服务端未设令牌时提示留空即可;未登录时提示需先登录门户。 - README 补充对应说明。
This commit is contained in:
@ -27,6 +27,8 @@ var (
|
||||
// 所以默认**不校验**;只有当你把容器端口直接暴露给不受信的网段、
|
||||
// 而没有外层认证时才设 NAVI_ADMIN_REQUIRE_TOKEN=1。
|
||||
requireTokenForWrite = isTruthy(os.Getenv("NAVI_ADMIN_REQUIRE_TOKEN"))
|
||||
// 浏览器插件同步用的令牌(供安装引导页展示,方便用户拷贝)
|
||||
syncToken = os.Getenv("NAVI_SYNC_TOKEN")
|
||||
// 本地上传的图标目录(Dashy 通过 /item-icons/ 读取)
|
||||
iconDir = envOr("NAVI_ICON_DIR", filepath.Join(filepath.Dir(confFile), "item-icons"))
|
||||
// 壁纸目录(nginx 通过 /wallpapers/ 提供)
|
||||
@ -456,6 +458,23 @@ func handleDeleteItem(w http.ResponseWriter, r *http.Request) {
|
||||
jsonResp(w, map[string]string{"status": "ok"})
|
||||
}
|
||||
|
||||
// handleSyncInfo 把浏览器插件需要的接入信息给已登录用户。
|
||||
//
|
||||
// 为何可以在这里返回令牌:/admin/ 整体在门户登录墙之内;
|
||||
// 而 /api/history/sync 是被门户放行的(否则插件无法上报),
|
||||
// 所以这个 token 仍然是有意义的凭据,并且不能挂在 /api/ 下暴露。
|
||||
func handleSyncInfo(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
jsonErr(w, 405, "method not allowed")
|
||||
return
|
||||
}
|
||||
jsonResp(w, map[string]any{
|
||||
"syncToken": syncToken,
|
||||
"hasToken": syncToken != "",
|
||||
"requireAdminToken": requireTokenForWrite,
|
||||
})
|
||||
}
|
||||
|
||||
// ── 站点信息:标题 / 副标题 / 顶部导航 / 页脚 ──
|
||||
// 这些东西原先只能在 conf.yml 里手改,现在可在 /admin/ 编辑。
|
||||
func handlePageInfo(w http.ResponseWriter, r *http.Request) {
|
||||
@ -628,6 +647,7 @@ func main() {
|
||||
http.HandleFunc("/api/pageinfo", cors(handlePageInfo))
|
||||
http.HandleFunc("/api/icon", cors(handleIconUpload))
|
||||
http.HandleFunc("/api/wallpaper", cors(handleWallpaper))
|
||||
http.HandleFunc("/api/sync-info", cors(handleSyncInfo))
|
||||
http.HandleFunc("/api/reload", cors(func(w http.ResponseWriter, r *http.Request) {
|
||||
jsonResp(w, map[string]string{"status": "saved"})
|
||||
}))
|
||||
|
||||
@ -42,6 +42,7 @@
|
||||
.site input{flex:1;min-width:240px;background:#0b1220;border:1px solid #334155;border-radius:10px;
|
||||
padding:9px 12px;color:#e2e8f0;font-size:14px}
|
||||
.hint{font-size:13px;color:#94a3b8;margin-top:10px}
|
||||
.lbl{display:block;font-size:12px;font-weight:600;color:#94a3b8;margin:12px 0 0}
|
||||
.done{font-size:13px;color:#86efac;margin-top:8px}
|
||||
</style>
|
||||
</head>
|
||||
@ -51,12 +52,22 @@
|
||||
<p class="sub">安装后浏览记录会自动同步到本站,并据此把最常访问的服务排到「常用服务」最前面。</p>
|
||||
|
||||
<div class="card">
|
||||
<h2>第一步:填写本站地址</h2>
|
||||
<p class="hint">在插件设置里粘贴下面这个地址(点「复制」)。同步令牌从部署方的 <code>NAVI_SYNC_TOKEN</code> 环境变量获取。</p>
|
||||
<h2>第一步:填写插件设置</h2>
|
||||
<p class="hint">在插件设置里填下面两项,各自点「复制」再粘贴。</p>
|
||||
|
||||
<label class="lbl" for="site-url">服务地址</label>
|
||||
<div class="site">
|
||||
<input id="site-url" readonly>
|
||||
<button class="btn" id="copy-btn" type="button">复制</button>
|
||||
</div>
|
||||
|
||||
<label class="lbl" for="site-token">同步令牌</label>
|
||||
<div class="site">
|
||||
<input id="site-token" readonly placeholder="读取中…">
|
||||
<button class="btn" id="copy-token" type="button">复制</button>
|
||||
</div>
|
||||
<p class="hint" id="token-note"></p>
|
||||
|
||||
<div class="done" id="copy-done" hidden>已复制</div>
|
||||
</div>
|
||||
|
||||
@ -127,18 +138,54 @@
|
||||
|
||||
document.getElementById('site-url').value = location.origin;
|
||||
|
||||
document.getElementById('copy-btn').addEventListener('click', function () {
|
||||
var input = document.getElementById('site-url');
|
||||
var doneTimer = null;
|
||||
function flashDone() {
|
||||
var done = document.getElementById('copy-done');
|
||||
done.hidden = false;
|
||||
if (doneTimer) clearTimeout(doneTimer);
|
||||
doneTimer = setTimeout(function () { done.hidden = true; }, 2000);
|
||||
}
|
||||
|
||||
function copyFrom(inputId) {
|
||||
var input = document.getElementById(inputId);
|
||||
if (!input || !input.value) return;
|
||||
input.select();
|
||||
input.setSelectionRange(0, 999);
|
||||
var done = document.getElementById('copy-done');
|
||||
function ok() { done.hidden = false; setTimeout(function () { done.hidden = true; }, 2000); }
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
navigator.clipboard.writeText(input.value).then(ok, function () { document.execCommand('copy'); ok(); });
|
||||
navigator.clipboard.writeText(input.value).then(flashDone, function () {
|
||||
document.execCommand('copy'); flashDone();
|
||||
});
|
||||
} else {
|
||||
document.execCommand('copy'); ok();
|
||||
document.execCommand('copy'); flashDone();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById('copy-btn').addEventListener('click', function () { copyFrom('site-url'); });
|
||||
document.getElementById('copy-token').addEventListener('click', function () { copyFrom('site-token'); });
|
||||
|
||||
/* 同步令牌:由 admin-server 提供(/admin/ 在门户登录墙之内,
|
||||
所以令牌不会出现在被门户放行的 /api/ 路径上)。 */
|
||||
fetch('/admin/api/sync-info', { cache: 'no-store' })
|
||||
.then(function (r) { return r.ok ? r.json() : null; })
|
||||
.then(function (d) {
|
||||
var tk = document.getElementById('site-token');
|
||||
var note = document.getElementById('token-note');
|
||||
if (d && d.hasToken) {
|
||||
tk.value = d.syncToken;
|
||||
note.textContent = '把这两项填进插件设置即可。';
|
||||
} else if (d) {
|
||||
tk.value = '';
|
||||
tk.placeholder = '(服务端未设置令牌)';
|
||||
document.getElementById('copy-token').hidden = true;
|
||||
note.textContent = '服务端未配置 NAVI_SYNC_TOKEN,令牌留空即可。';
|
||||
} else {
|
||||
tk.placeholder = '无法读取,请确认已登录';
|
||||
note.textContent = '需要登录门户后才能读取令牌。';
|
||||
}
|
||||
})
|
||||
.catch(function () {
|
||||
document.getElementById('site-token').placeholder = '无法读取,请确认已登录';
|
||||
});
|
||||
|
||||
/* ── 浏览器识别 ──
|
||||
UA 会互相包含(Edge/Brave/Opera 的 UA 里都有 Chrome/),所以要按
|
||||
|
||||
Reference in New Issue
Block a user