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>
This commit is contained in:
2026-07-20 09:44:41 +02:00
co-authored by Claude
parent 1e9618c920
commit ddf909502a
9 changed files with 278 additions and 80 deletions
+26 -15
View File
@@ -1,36 +1,40 @@
const DEFAULT_TRACKER_URL = 'http://localhost:3000';
// Tracker-Adresse ist fest (gehostete Instanz). Jeder Benutzer gibt seinen
// eigenen REST-API-Key ein (X-API-Key), über den der Server den Import dem
// richtigen Benutzerkonto zuordnet — siehe /api/indeed-import serverseitig.
const TRACKER_URL = 'https://nextjobs.cc';
const $ = (id) => document.getElementById(id);
const statusEl = $('status');
function normalize(url) {
return (url || '').trim().replace(/\/+$/, '');
}
function setStatus(html, cls) {
statusEl.innerHTML = html;
statusEl.className = cls || '';
}
// ---- Settings (tracker URL) ------------------------------------------------
// ---- Settings (API-Key) ----------------------------------------------------
chrome.storage.sync.get({ trackerUrl: DEFAULT_TRACKER_URL }, (items) => {
$('trackerUrl').value = items.trackerUrl || DEFAULT_TRACKER_URL;
chrome.storage.sync.get({ apiKey: '' }, (items) => {
$('apiKey').value = items.apiKey || '';
});
$('saveBtn').addEventListener('click', () => {
const url = normalize($('trackerUrl').value) || DEFAULT_TRACKER_URL;
chrome.storage.sync.set({ trackerUrl: url }, () => {
$('trackerUrl').value = url;
setStatus('Einstellungen gespeichert.', 'ok');
const key = $('apiKey').value.trim();
chrome.storage.sync.set({ apiKey: key }, () => {
setStatus(key ? 'API-Key gespeichert.' : 'API-Key entfernt.', key ? 'ok' : '');
});
});
$('testBtn').addEventListener('click', async () => {
const url = normalize($('trackerUrl').value) || DEFAULT_TRACKER_URL;
const key = $('apiKey').value.trim();
if (!key) { setStatus('Bitte erst API-Key eingeben.', 'err'); return; }
setStatus('Teste Verbindung …', '');
try {
const res = await fetch(url + '/api/settings', { method: 'GET' });
setStatus(res.ok ? 'Verbindung erfolgreich ✓' : 'Erreichbar, aber unerwartete Antwort (' + res.status + ').', res.ok ? 'ok' : 'err');
const res = await fetch(TRACKER_URL + '/api/v1/health', {
headers: { 'X-API-Key': key },
});
if (res.ok) setStatus('Verbindung erfolgreich ✓ — Key gültig.', 'ok');
else if (res.status === 401) setStatus('Key abgelehnt (401). Bitte API-Key prüfen.', 'err');
else setStatus('Erreichbar, aber unerwartete Antwort (' + res.status + ').', 'err');
} catch (err) {
setStatus('Nicht erreichbar: ' + err.message, 'err');
}
@@ -158,6 +162,13 @@ function sendJob(payload) {
setStatus('Fehler: ' + chrome.runtime.lastError.message, 'err');
return;
}
if (resp && resp.needKey) {
$('sendBtn').disabled = false;
setStatus('Bitte erst in den Einstellungen deinen API-Key hinterlegen.', 'err');
const s = document.getElementById('settings'); if (s) s.open = true;
$('apiKey').focus();
return;
}
if (resp && resp.duplicate) {
$('sendBtn').disabled = false;
if (confirm(duplicateWarning(resp.matches))) {
@@ -198,4 +209,4 @@ $('sendBtn').addEventListener('click', () => {
$('rescanBtn').addEventListener('click', scanActivePage);
// Auto-extract as soon as the popup opens.
scanActivePage();
scanActivePage();