Files
eship/Extension/background.js

333 lines
9.7 KiB
JavaScript

const STORAGE_KEY = "auth_jwt";
const BACKEND_URL = "http://localhost:5173"; // TODO: Backend URL konfigurieren
const PARSE_TIMEOUT_MS = 15000; // 15 seconds
const SCAN_TIMEOUT_MS = 20000; // 20 seconds (seller listing pages can be slower)
const activeParseRequests = new Map(); // Map<tabId, { timeout, originalSender, resolve }>
const activeScanRequests = new Map(); // Map<tabId, { timeout, sendResponse }>
// Messages from content script (der von der Web-App kommt)
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
// Auth messages vom Content Script
if (msg?.type === "AUTH_JWT" && msg.jwt) {
chrome.storage.local.set({ [STORAGE_KEY]: msg.jwt }).then(() => {
sendResponse({ ok: true });
});
return true; // async
}
if (msg?.type === "AUTH_CLEARED") {
chrome.storage.local.remove(STORAGE_KEY).then(() => {
sendResponse({ ok: true });
});
return true;
}
// API calls vom Popup
if (msg?.type === "GET_JWT") {
getJwt().then(jwt => {
sendResponse({ jwt });
});
return true;
}
if (msg?.type === "CALL_API" && msg.path) {
callProtectedApi(msg.path, msg.payload).then(data => {
sendResponse({ ok: true, data });
}).catch(err => {
sendResponse({ ok: false, error: err.message });
});
return true;
}
// eBay Parsing Request (from Web App via content script or directly)
if (msg?.action === "PARSE_URL" && msg.url) {
handleParseRequest(msg.url, sendResponse);
return true; // async
}
// eBay Parsing Response (from eBay content script)
if (msg?.action === "PARSE_COMPLETE") {
handleParseComplete(sender.tab?.id, msg.data);
return true;
}
});
// Handle messages from external web apps (externally_connectable)
chrome.runtime.onMessageExternal.addListener((msg, sender, sendResponse) => {
if (msg?.action === "PARSE_URL" && msg.url) {
handleParseRequest(msg.url, sendResponse);
return true; // async
}
if (msg?.action === "SCAN_PRODUCTS" && msg.url && msg.accountId) {
handleScanProductsRequest(msg.url, msg.accountId, sendResponse);
return true; // async
}
});
/**
* Handles eBay URL parsing request
* Creates a hidden tab, waits for load, sends parse message to content script
*/
async function handleParseRequest(url, sendResponse) {
try {
// Validate URL
if (!url || typeof url !== 'string' || !url.toLowerCase().includes('ebay.')) {
sendResponse({ ok: false, error: "Invalid eBay URL" });
return;
}
// Create hidden tab
const tab = await chrome.tabs.create({
url: url,
active: false
});
const tabId = tab.id;
// Set up timeout
const timeoutId = setTimeout(() => {
cleanupParseRequest(tabId, null, { ok: false, error: "timeout" });
}, PARSE_TIMEOUT_MS);
// Store request info
activeParseRequests.set(tabId, {
timeout: timeoutId,
sendResponse: sendResponse
});
// Wait for tab to load, then send parse message
const checkTabLoaded = (updatedTabId, changeInfo, updatedTab) => {
if (updatedTabId !== tabId) return;
// Tab is fully loaded
if (changeInfo.status === 'complete' && updatedTab.url) {
chrome.tabs.onUpdated.removeListener(checkTabLoaded);
// Small delay to ensure DOM is ready
setTimeout(() => {
// Send parse message to content script
chrome.tabs.sendMessage(tabId, { action: "PARSE_EBAY" })
.then(response => {
if (response && response.ok && response.data) {
handleParseComplete(tabId, response.data);
} else {
cleanupParseRequest(tabId, null, { ok: false, error: "Parsing failed" });
}
})
.catch(err => {
console.error("Error sending parse message:", err);
cleanupParseRequest(tabId, null, { ok: false, error: "Content script error" });
});
}, 1000); // 1 second delay for DOM ready
}
};
chrome.tabs.onUpdated.addListener(checkTabLoaded);
} catch (error) {
console.error("Error in handleParseRequest:", error);
sendResponse({ ok: false, error: error.message || "Unknown error" });
}
}
/**
* Handles parse complete response from content script
*/
function handleParseComplete(tabId, data) {
cleanupParseRequest(tabId, data, null);
}
/**
* Cleans up parse request: closes tab, clears timeout, sends response
*/
async function cleanupParseRequest(tabId, data, error) {
const request = activeParseRequests.get(tabId);
if (!request) return;
// Clear timeout
if (request.timeout) {
clearTimeout(request.timeout);
}
// Remove from active requests
activeParseRequests.delete(tabId);
// Close tab
try {
await chrome.tabs.remove(tabId);
} catch (err) {
// Tab might already be closed
console.warn("Could not close tab:", err);
}
// Send response
if (request.sendResponse) {
if (error) {
request.sendResponse(error);
} else if (data) {
request.sendResponse({ ok: true, data: data });
} else {
request.sendResponse({ ok: false, error: "Unknown error" });
}
}
}
/**
* Handles eBay product scan request
* Creates a hidden tab, waits for load, sends parse message to content script
*/
async function handleScanProductsRequest(url, accountId, sendResponse) {
try {
// Validate URL
if (!url || typeof url !== 'string' || !url.toLowerCase().includes('ebay.')) {
sendResponse({ ok: false, error: "Invalid eBay URL" });
return;
}
// Create hidden tab
const tab = await chrome.tabs.create({
url: url,
active: false
});
const tabId = tab.id;
// Set up timeout
const timeoutId = setTimeout(() => {
cleanupScanRequest(tabId, null, { ok: false, error: "timeout" });
}, SCAN_TIMEOUT_MS);
// Store request info
activeScanRequests.set(tabId, {
timeout: timeoutId,
sendResponse: sendResponse
});
// Wait for tab to load, then send parse message
const checkTabLoaded = (updatedTabId, changeInfo, updatedTab) => {
if (updatedTabId !== tabId) return;
// Tab is fully loaded
if (changeInfo.status === 'complete' && updatedTab.url) {
chrome.tabs.onUpdated.removeListener(checkTabLoaded);
// Small delay to ensure DOM is ready
setTimeout(() => {
// Send parse message to content script
chrome.tabs.sendMessage(tabId, { action: "PARSE_PRODUCT_LIST" })
.then(response => {
if (response && response.ok) {
// Prüfe ob items vorhanden und nicht leer
const items = response.items || response.data?.items || [];
const meta = response.meta || response.data?.meta || {};
// Log meta für Debugging
console.log("[SCAN meta]", meta);
if (items.length === 0) {
// Leere items: behandele als Fehler mit meta
cleanupScanRequest(tabId, null, {
ok: false,
error: "empty_items",
meta: meta
});
} else {
// Erfolg: sende items + meta
handleScanComplete(tabId, { items, meta });
}
} else {
// Fehler: sende error + meta
const meta = response?.meta || {};
console.log("[SCAN meta]", meta);
cleanupScanRequest(tabId, null, {
ok: false,
error: response?.error || "Parsing failed",
meta: meta
});
}
})
.catch(err => {
console.error("Error sending parse message:", err);
cleanupScanRequest(tabId, null, { ok: false, error: "Content script error" });
});
}, 1000); // 1 second delay for DOM ready
}
};
chrome.tabs.onUpdated.addListener(checkTabLoaded);
} catch (error) {
console.error("Error in handleScanProductsRequest:", error);
sendResponse({ ok: false, error: error.message || "Unknown error" });
}
}
/**
* Handles scan complete response from content script
*/
function handleScanComplete(tabId, data) {
cleanupScanRequest(tabId, data, null);
}
/**
* Cleans up scan request: closes tab, clears timeout, sends response
*/
async function cleanupScanRequest(tabId, data, error) {
const request = activeScanRequests.get(tabId);
if (!request) return;
// Clear timeout
if (request.timeout) {
clearTimeout(request.timeout);
}
// Remove from active requests
activeScanRequests.delete(tabId);
// Close tab (always, even on error)
try {
await chrome.tabs.remove(tabId);
} catch (err) {
// Tab might already be closed
console.warn("Could not close tab:", err);
}
// Send response
if (request.sendResponse) {
if (error) {
// error kann bereits meta enthalten
request.sendResponse(error);
} else if (data) {
// data kann items + meta enthalten
request.sendResponse({ ok: true, data: data });
} else {
request.sendResponse({ ok: false, error: "Unknown error" });
}
}
}
export async function getJwt() {
const data = await chrome.storage.local.get(STORAGE_KEY);
return data[STORAGE_KEY] || "";
}
export async function callProtectedApi(path, payload) {
const jwt = await getJwt();
if (!jwt) throw new Error("Not authed");
const res = await fetch(`${BACKEND_URL}${path}`, {
method: "POST",
headers: {
"content-type": "application/json",
"authorization": `Bearer ${jwt}`
},
body: JSON.stringify(payload || {})
});
if (!res.ok) throw new Error(`API error: ${res.status}`);
return await res.json();
}