- Include dist-release/ and release/ for direct colleague use - Add beginner-friendly installation guide - Update .gitignore to track distribution builds
220 lines
7.8 KiB
JavaScript
220 lines
7.8 KiB
JavaScript
"use strict";
|
|
(() => {
|
|
// src/popup/view.ts
|
|
function renderLoggedOut(root, error) {
|
|
root.innerHTML = `
|
|
<section data-popup-state="logged-out">
|
|
<h1>Star Chart Search Enhancer</h1>
|
|
<p>\u767B\u5F55\u540E\u624D\u80FD\u4F7F\u7528\u661F\u56FE\u589E\u5F3A\u529F\u80FD</p>
|
|
${error ? `<p data-popup-error="true">${error}</p>` : ""}
|
|
<button type="button" data-popup-sign-in="button">\u767B\u5F55 Logto</button>
|
|
</section>
|
|
`;
|
|
}
|
|
function renderLoggedIn(root, authState) {
|
|
const userInfo = authState.userInfo;
|
|
root.innerHTML = `
|
|
<section data-popup-state="logged-in">
|
|
<h1>Star Chart Search Enhancer</h1>
|
|
<p>\u5DF2\u767B\u5F55</p>
|
|
<p>${userInfo?.name ?? userInfo?.username ?? "\u672A\u77E5\u7528\u6237"}</p>
|
|
<p>${userInfo?.email ?? ""}</p>
|
|
<button type="button" data-popup-sign-out="button">\u9000\u51FA\u767B\u5F55</button>
|
|
</section>
|
|
`;
|
|
}
|
|
function renderDevPanel(root, authState) {
|
|
const panel = root.ownerDocument.createElement("section");
|
|
panel.dataset.popupDevPanel = "root";
|
|
panel.innerHTML = `
|
|
<h2>dev auth panel</h2>
|
|
<p>resource: ${authState.resource ?? ""}</p>
|
|
<p>scopes: ${(authState.scopes ?? []).join(", ")}</p>
|
|
<p>token: ${authState.tokenAvailable ? "available" : "missing"}</p>
|
|
<p>expires: ${authState.accessTokenExpiresAt ?? "unknown"}</p>
|
|
<p>error: ${authState.lastError ?? ""}</p>
|
|
<button type="button" data-popup-test-protected-api="button">\u6D4B\u8BD5\u53D7\u4FDD\u62A4\u63A5\u53E3</button>
|
|
<pre data-popup-protected-api-result="output"></pre>
|
|
`;
|
|
root.appendChild(panel);
|
|
}
|
|
function setProtectedApiResult(root, value) {
|
|
const output = root.querySelector(
|
|
'[data-popup-protected-api-result="output"]'
|
|
);
|
|
if (!output) {
|
|
return;
|
|
}
|
|
output.textContent = value;
|
|
}
|
|
|
|
// src/shared/auth-config.ts
|
|
var defaultAuthConfig = {
|
|
apiResource: "https://talent-search.intelligrow.cn",
|
|
appId: "i4jkllbvih0554r4n0fd3",
|
|
enableDevAuthPanel: false,
|
|
logtoEndpoint: "https://login-api.intelligrow.cn",
|
|
scopes: ["openid", "profile", "offline_access", "talent-search:read"]
|
|
};
|
|
function readAuthConfig(overrides = {}) {
|
|
const nextConfig = {
|
|
...defaultAuthConfig,
|
|
...overrides
|
|
};
|
|
if (!nextConfig.logtoEndpoint.trim()) {
|
|
throw new Error("auth config logtoEndpoint is required");
|
|
}
|
|
if (!nextConfig.appId.trim()) {
|
|
throw new Error("auth config appId is required");
|
|
}
|
|
if (!nextConfig.apiResource.trim()) {
|
|
throw new Error("auth config apiResource is required");
|
|
}
|
|
return nextConfig;
|
|
}
|
|
|
|
// src/shared/auth-messages.ts
|
|
function isAuthResponseMessage(value) {
|
|
if (!value || typeof value !== "object") {
|
|
return false;
|
|
}
|
|
const candidate = value;
|
|
if (candidate.ok === false) {
|
|
return candidate.type === "auth:error" && typeof candidate.error === "string";
|
|
}
|
|
if (candidate.ok !== true || typeof candidate.type !== "string") {
|
|
return false;
|
|
}
|
|
if (candidate.type === "auth:ack") {
|
|
return true;
|
|
}
|
|
if (candidate.type === "auth:token") {
|
|
return Boolean(
|
|
candidate.value && typeof candidate.value === "object" && typeof candidate.value.accessToken === "string"
|
|
);
|
|
}
|
|
if (candidate.type === "auth:state") {
|
|
return Boolean(
|
|
candidate.value && typeof candidate.value === "object" && typeof candidate.value.isAuthenticated === "boolean"
|
|
);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// src/shared/protected-api-client.ts
|
|
function createProtectedApiClient(options) {
|
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
return {
|
|
async loadProtectedMockData() {
|
|
const token = await readAccessToken(options.sendMessage);
|
|
const response = await fetchImpl(
|
|
new URL("/api/mock/protected", options.baseUrl).toString(),
|
|
{
|
|
headers: {
|
|
Authorization: `Bearer ${token}`
|
|
},
|
|
method: "GET"
|
|
}
|
|
);
|
|
if (response.status === 401 || response.status === 403) {
|
|
throw new Error("protected api unauthorized");
|
|
}
|
|
if (!response.ok) {
|
|
throw new Error(`protected api request failed: ${response.status}`);
|
|
}
|
|
return response.json();
|
|
}
|
|
};
|
|
}
|
|
async function readAccessToken(sendMessage) {
|
|
const response = await sendMessage({ type: "auth:get-access-token" });
|
|
if (!isAuthResponseMessage(response) || !response.ok || response.type !== "auth:token" || !response.value.accessToken.trim()) {
|
|
throw new Error("protected api token unavailable");
|
|
}
|
|
return response.value.accessToken;
|
|
}
|
|
|
|
// src/popup/index.ts
|
|
async function bootPopup(options = {}) {
|
|
const currentDocument = options.document ?? document;
|
|
const popupConfig = readAuthConfig(options.config);
|
|
const root = currentDocument.querySelector("#app");
|
|
const HTMLElementCtor = currentDocument.defaultView?.HTMLElement;
|
|
if (!root || HTMLElementCtor && !(root instanceof HTMLElementCtor)) {
|
|
throw new Error("popup root #app is required");
|
|
}
|
|
const sendMessage = options.sendMessage ?? ((message) => Promise.resolve(
|
|
globalThis.chrome?.runtime?.sendMessage?.(message)
|
|
));
|
|
const fetchProtectedApi = options.fetchProtectedApi ?? createProtectedApiClient({
|
|
baseUrl: "http://127.0.0.1:4319",
|
|
sendMessage
|
|
}).loadProtectedMockData;
|
|
await renderCurrentAuthState(root, popupConfig, sendMessage, fetchProtectedApi);
|
|
}
|
|
async function renderCurrentAuthState(root, popupConfig, sendMessage, fetchProtectedApi) {
|
|
const response = await sendMessage({ type: "auth:get-state" });
|
|
if (!isAuthResponseMessage(response) || !response.ok || response.type !== "auth:state") {
|
|
renderLoggedOut(root, "\u8BA4\u8BC1\u72B6\u6001\u8BFB\u53D6\u5931\u8D25");
|
|
return;
|
|
}
|
|
if (!response.value.isAuthenticated) {
|
|
renderLoggedOut(root, response.value.lastError);
|
|
root.querySelector('[data-popup-sign-in="button"]')?.addEventListener("click", () => {
|
|
void runAuthAction(root, popupConfig, sendMessage, {
|
|
actionMessage: { type: "auth:sign-in" },
|
|
fetchProtectedApi
|
|
});
|
|
});
|
|
return;
|
|
}
|
|
renderLoggedIn(root, response.value);
|
|
root.querySelector('[data-popup-sign-out="button"]')?.addEventListener("click", () => {
|
|
void runAuthAction(root, popupConfig, sendMessage, {
|
|
actionMessage: { type: "auth:sign-out" },
|
|
fetchProtectedApi
|
|
});
|
|
});
|
|
if (popupConfig.enableDevAuthPanel) {
|
|
renderDevPanel(root, response.value);
|
|
root.querySelector('[data-popup-test-protected-api="button"]')?.addEventListener("click", () => {
|
|
void runProtectedApiProbe(root, fetchProtectedApi);
|
|
});
|
|
}
|
|
}
|
|
async function runAuthAction(root, popupConfig, sendMessage, options) {
|
|
const response = await sendMessage(options.actionMessage);
|
|
if (isActionError(response)) {
|
|
renderLoggedOut(root, response.error);
|
|
root.querySelector('[data-popup-sign-in="button"]')?.addEventListener("click", () => {
|
|
void runAuthAction(root, popupConfig, sendMessage, options);
|
|
});
|
|
return;
|
|
}
|
|
await renderCurrentAuthState(
|
|
root,
|
|
popupConfig,
|
|
sendMessage,
|
|
options.fetchProtectedApi
|
|
);
|
|
}
|
|
function isActionError(response) {
|
|
return isAuthResponseMessage(response) && !response.ok && response.type === "auth:error";
|
|
}
|
|
async function runProtectedApiProbe(root, fetchProtectedApi) {
|
|
setProtectedApiResult(root, "\u8BF7\u6C42\u4E2D...");
|
|
try {
|
|
const result = await fetchProtectedApi();
|
|
setProtectedApiResult(root, JSON.stringify(result, null, 2));
|
|
} catch (error) {
|
|
setProtectedApiResult(
|
|
root,
|
|
error instanceof Error ? error.message : String(error)
|
|
);
|
|
}
|
|
}
|
|
if (typeof document !== "undefined") {
|
|
void bootPopup();
|
|
}
|
|
})();
|