172 lines
5.3 KiB
JavaScript
172 lines
5.3 KiB
JavaScript
import { normalizeBaseUrl, vaultFetch } from "./lib/api.js";
|
||||
|
|
import { loadConfig, loadPersistent } from "./lib/storage.js";
|
|||
|
|
|
|||
|
|
const $ = (id) => document.getElementById(id);
|
|||
|
|
|
|||
|
|
function setStatus(text, kind) {
|
|||
|
|
const el = $("status");
|
|||
|
|
el.textContent = text || "";
|
|||
|
|
el.className = kind || "";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function setWorkspaceHint(rootUri, rootPath, source) {
|
|||
|
|
const el = $("workspaceHint");
|
|||
|
|
if (!el) return;
|
|||
|
|
if (!rootUri) {
|
|||
|
|
el.textContent = "";
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
const path = rootPath || rootUri.replace(/^file:\/\//, "");
|
|||
|
|
const src =
|
|||
|
|
source === "default"
|
|||
|
|
? "账号默认工作区"
|
|||
|
|
: source === "override"
|
|||
|
|
? "手动覆盖"
|
|||
|
|
: "已保存";
|
|||
|
|
el.innerHTML = `密码箱写入:<code>${path}</code>(${src})`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 必须在用户点击手势内调用:MV3 从 service worker 弹权限常被静默拒绝,
|
|||
|
|
* 外网 baseUrl(含非标端口 frp)没有 host 权限时 fetch 就是 Failed to fetch。
|
|||
|
|
* 同时申请 http(s)://*/*,否则外站 Cookie 读不到、登录态无法保存。
|
|||
|
|
* @param {string} baseUrl
|
|||
|
|
*/
|
|||
|
|
async function requestBaseUrlPermission(baseUrl) {
|
|||
|
|
const origin = new URL(normalizeBaseUrl(baseUrl)).origin;
|
|||
|
|
// Broad first so cookie capture works on every login site after connect.
|
|||
|
|
const broad = ["http://*/*", "https://*/*", `${origin}/*`];
|
|||
|
|
try {
|
|||
|
|
const haveBroad = await chrome.permissions.contains({ origins: broad });
|
|||
|
|
if (haveBroad) return true;
|
|||
|
|
const ok = await chrome.permissions.request({ origins: broad });
|
|||
|
|
if (ok) return true;
|
|||
|
|
} catch {
|
|||
|
|
/* fall through to narrow */
|
|||
|
|
}
|
|||
|
|
const origins = [`${origin}/*`];
|
|||
|
|
const have = await chrome.permissions.contains({ origins });
|
|||
|
|
if (have) return true;
|
|||
|
|
try {
|
|||
|
|
return await chrome.permissions.request({ origins });
|
|||
|
|
} catch (e) {
|
|||
|
|
throw new Error(
|
|||
|
|
`无法申请访问权限 ${origin}:${e?.message || e}(请在扩展详情里允许该站点;登录态还需「所有网站」)`
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function formatFetchError(e, baseUrl) {
|
|||
|
|
const msg = e?.message || String(e);
|
|||
|
|
if (/Failed to fetch|NetworkError|Load failed/i.test(msg)) {
|
|||
|
|
return (
|
|||
|
|
`无法访问 ${baseUrl || "baseUrl"}:${msg}。` +
|
|||
|
|
`常见原因:① 未点允许扩展访问该站点 ② 外网隧道未通 ③ 证书不受信任。` +
|
|||
|
|
`请重新加载扩展后再点「测试连接」,并允许弹窗中的站点权限。`
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
return msg;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function hydrate() {
|
|||
|
|
const p = await loadPersistent();
|
|||
|
|
if (p.baseUrl) $("baseUrl").value = p.baseUrl;
|
|||
|
|
// 默认不展示手填;仅当用户曾覆盖时回填并展开
|
|||
|
|
if (p.rootUri) {
|
|||
|
|
$("rootUri").value = p.rootUri;
|
|||
|
|
const adv = $("advRoot");
|
|||
|
|
if (adv && p.rootUri) {
|
|||
|
|
// 不自动展开;连接后 hint 会显示实际路径
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
if (p.email) $("email").value = p.email;
|
|||
|
|
const cfg = await loadConfig();
|
|||
|
|
if (cfg.connected) {
|
|||
|
|
setStatus(
|
|||
|
|
`已连接 · ${cfg.email || cfg.userId || ""} · token 有效`,
|
|||
|
|
"ok"
|
|||
|
|
);
|
|||
|
|
setWorkspaceHint(cfg.rootUri || p.rootUri, p.rootPath, "saved");
|
|||
|
|
} else if (cfg.baseUrl) {
|
|||
|
|
setStatus("已保存配置,但 token 缺失(浏览器重启后需重新登录)", "err");
|
|||
|
|
setWorkspaceHint(p.rootUri, p.rootPath, "saved");
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$("test").onclick = async () => {
|
|||
|
|
setStatus("测试中…");
|
|||
|
|
let baseUrl = "";
|
|||
|
|
try {
|
|||
|
|
baseUrl = normalizeBaseUrl($("baseUrl").value);
|
|||
|
|
const ok = await requestBaseUrlPermission(baseUrl);
|
|||
|
|
if (!ok) {
|
|||
|
|
setStatus(
|
|||
|
|
`已取消访问权限:需要允许扩展访问 ${new URL(baseUrl).origin}`,
|
|||
|
|
"err"
|
|||
|
|
);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
// whoami 不依赖 /api/health(外网入口可能未挂 health)
|
|||
|
|
const data = await vaultFetch(baseUrl, "/api/auth/whoami", {
|
|||
|
|
method: "GET",
|
|||
|
|
credentials: true,
|
|||
|
|
});
|
|||
|
|
setStatus(
|
|||
|
|
`可达 · ${baseUrl} · whoami ok=${Boolean(data?.ok ?? true)}`,
|
|||
|
|
"ok"
|
|||
|
|
);
|
|||
|
|
} catch (e) {
|
|||
|
|
setStatus(formatFetchError(e, baseUrl || $("baseUrl").value), "err");
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
$("connect").onclick = async () => {
|
|||
|
|
setStatus("登录中…");
|
|||
|
|
setWorkspaceHint("", "");
|
|||
|
|
const password = $("password").value;
|
|||
|
|
let baseUrl = "";
|
|||
|
|
try {
|
|||
|
|
baseUrl = normalizeBaseUrl($("baseUrl").value);
|
|||
|
|
// 关键:在点击手势内申请 host 权限,再交给 background 登录
|
|||
|
|
const ok = await requestBaseUrlPermission(baseUrl);
|
|||
|
|
if (!ok) {
|
|||
|
|
setStatus(
|
|||
|
|
`已取消访问权限:需要允许扩展访问 ${new URL(baseUrl).origin}(含端口)`,
|
|||
|
|
"err"
|
|||
|
|
);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
const res = await chrome.runtime.sendMessage({
|
|||
|
|
type: "CONNECT",
|
|||
|
|
baseUrl,
|
|||
|
|
// 留空 → background 调 default workspace
|
|||
|
|
rootUri: ($("rootUri").value || "").trim(),
|
|||
|
|
email: $("email").value,
|
|||
|
|
password,
|
|||
|
|
});
|
|||
|
|
$("password").value = "";
|
|||
|
|
if (!res?.ok) {
|
|||
|
|
setStatus(res?.error || "连接失败", "err");
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
const r = res.result || {};
|
|||
|
|
if (r.rootUri) $("rootUri").value = r.rootUri;
|
|||
|
|
setStatus(
|
|||
|
|
`已连接 · ${r.email || r.userId || ""} · 过期 ${r.expiresAt || "—"}`,
|
|||
|
|
"ok"
|
|||
|
|
);
|
|||
|
|
setWorkspaceHint(r.rootUri, r.rootPath, r.workspaceSource || "default");
|
|||
|
|
} catch (e) {
|
|||
|
|
$("password").value = "";
|
|||
|
|
setStatus(formatFetchError(e, baseUrl || $("baseUrl").value), "err");
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
$("disconnect").onclick = async () => {
|
|||
|
|
await chrome.runtime.sendMessage({ type: "DISCONNECT" });
|
|||
|
|
setStatus("已断开", "err");
|
|||
|
|
setWorkspaceHint("", "");
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
hydrate();
|