Files
thomasandClaude ddf909502a Browser-Plugin: Multi-User/X-API-Key + Footer-Download
Plugin (Manifest V3) refactored für gehostete Multi-User-Instanz:
- Tracker-URL hardcoded https://nextjobs.cc (keine URL-Eingabe mehr)
- Authentifizierung ausschließlich über persönlichen REST-API-Key
  (Header X-API-Key), pro Benutzer in chrome.storage.sync
- Server: /api/indeed-import nutzt neuen requireApiKey-Middleware,
  ordnet Import dem Benutzerkonto mit passendem cfg:API_TOKEN zu

Download: /plugin/nextjobs-stellen-import.zip packt extension/ on the
fly (dep-freier STORE-Zip in lib/zipdir.js, deterministischer Timestamp),
route hinter requireAuth. Footer-Link im App-Footer. Dockerfile kopiert
extension/ ins Image.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-20 09:44:41 +02:00

77 lines
2.7 KiB
JavaScript

// Service worker: performs the cross-origin POST to NextJobs.
// Routing the request through the background worker (instead of a content
// script) keeps it independent of the page's Content-Security-Policy.
//
// Die Tracker-Adresse ist fest (https://nextjobs.cc). Die Authentifizierung
// läuft über den persönlichen REST-API-Key (Header X-API-Key), den jeder
// Benutzer im Popup hinterlegt — der Server ordnet den Import damit dem
// richtigen Benutzerkonto zu. Ohne Key lehnt der Server mit 401 ab.
const TRACKER_URL = 'https://nextjobs.cc';
function getApiKey() {
return new Promise((resolve) => {
chrome.storage.sync.get({ apiKey: '' }, (items) => {
resolve((items.apiKey || '').trim());
});
});
}
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message && message.type === 'IMPORT_JOB') {
(async () => {
try {
const apiKey = await getApiKey();
if (!apiKey) {
sendResponse({ ok: false, needKey: true, error: 'Kein API-Key hinterlegt.' });
return;
}
const res = await fetch(TRACKER_URL + '/api/indeed-import', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-API-Key': apiKey },
body: JSON.stringify(message.payload),
});
let data = {};
try { data = await res.json(); } catch (_) { /* non-JSON response */ }
if (!res.ok) {
// Duplicate guard: surface the existing matches so the user can decide.
if (res.status === 409 && data && data.duplicate) {
sendResponse({
ok: false,
duplicate: true,
matches: data.matches || [],
error: (data && data.error) || 'Für diese Stelle existiert bereits eine Bewerbung.',
});
return;
}
if (res.status === 401) {
sendResponse({ ok: false, needKey: true, error: 'API-Key abgelehnt (401). Bitte in den Einstellungen prüfen.' });
return;
}
sendResponse({
ok: false,
error: (data && data.error) || `Server antwortete mit ${res.status}`,
});
return;
}
sendResponse({
ok: true,
id: data.id,
message: data.message,
openUrl: data.url ? TRACKER_URL + data.url : null,
});
} catch (err) {
sendResponse({
ok: false,
error:
'Konnte NextJobs nicht erreichen (' + String(err.message) + '). ' +
'Prüfe deine Internetverbindung und ob der API-Key in den Einstellungen korrekt ist.',
});
}
})();
return true; // keep the message channel open for the async response
}
});