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
+48 -37
View File
@@ -1,50 +1,61 @@
# NextJobs Indeed Import (Browser-Erweiterung)
# NextJobs Stellen-Import (Browser-Erweiterung)
Chromium-Erweiterung (Manifest V3), die auf Indeed einen Button **direkt neben der
Stellenbeschreibung** einfügt. Ein Klick sendet alle Stelleninformationen an den
NextJobs. Dort wird die Stelle als **Entwurf** angelegt und die KI
erstellt automatisch zugeschnittene Bewerbungsunterlagen (auf Basis der hinterlegten
Vorlagen).
Chromium-Erweiterung (Manifest V3), die **auf jeder Webseite** Stellen
ausliest und an NextJobs (`https://nextjobs.cc`) sendet. Auf Indeed wird
zusätzlich ein Button *direkt neben der Stellenbeschreibung* eingeblendet.
Die Stelle wird im Tracker als **Entwurf** angelegt; die KI erstellt daraus
zugeschnittene Bewerbungsunterlagen (auf Basis der hinterlegten Vorlagen).
## Multi-User & Authentifizierung
Die Tracker-Adresse ist fest `https://nextjobs.cc` (hardcodiert). Jeder
Benutzer authentifiziert sich mit seinem **persönlichen REST-API-Key**:
- Im Tracker unter **Einstellungen → API-Token (X-API-Key)** einen Token
setzen (falls noch keiner da ist: neu vergeben).
- In der Erweiterung auf das Symbol klicken → **Einstellungen** → Token
eintragen → **Speichern** + **Verbindung testen**.
Der Key wird im `X-API-Key`-Header gesendet; der Server ordnet den Import dem
Benutzerkonto zu, das diesen Token hinterlegt hat. Ohne (oder mit ungültigem)
Key antwortet der Server mit **401**. Der Key wird pro Browser-Profil in der
`chrome.storage.sync` gespeichert — wer mehrere Konten verwaltet, kann den Key
jederzeit in den Einstellungen wechseln.
## Installation (Entwicklermodus)
1. NextJobs starten (Standard: `http://localhost:3000`).
2. In Chrome/Edge/Brave `chrome://extensions` öffnen.
3. **Entwicklermodus** oben rechts aktivieren.
4. **Entpackte Erweiterung laden** klicken und diesen `extension/`-Ordner auswählen.
## Konfiguration
Auf das Erweiterungs-Symbol klicken und die **Tracker-Adresse** setzen
(z. B. `http://localhost:3000` oder die Adresse deiner Deployment-Instanz).
Mit **Verbindung testen** prüfen, ob der Tracker erreichbar ist.
1. In Chrome/Edge/Brave `chrome://extensions` öffnen.
2. **Entwicklermodus** oben rechts aktivieren.
3. **Entpackte Erweiterung laden** klicken und diesen `extension/`-Ordner
auswählen.
4. API-Key eintragen (siehe oben).
## Verwendung
1. Auf `indeed.com` eine Stelle öffnen.
2. Neben der Überschrift **„Vollständige Stellenbeschreibung"** erscheint der grüne
Button **„An NextJobs senden"**.
3. Klicken → die Stelle wird als Entwurf angelegt, die Unterlagen werden im
Hintergrund generiert. Über den Link „Entwurf öffnen" gelangst du direkt zur
Bewerbung im Tracker.
1. Auf einer Stellenanzeige das Erweiterungs-Symbol klicken. Die Felder werden
automatisch ausgelesen (Schema.org/JSON-LD → OpenGraph → Auswahl/Text).
Text auf der Seite markieren → wird als Beschreibung übernommen.
2. Felder prüfen, ggf. korrigieren und **An Tracker senden** klicken.
Auf Indeed alternativ den grünen Button **„An NextJobs senden"** neben
der Stellenbeschreibung.
3. Über den Link **Entwurf öffnen** gelangst du direkt zur Bewerbung im Tracker.
## Voraussetzungen im Tracker
- Unter **Vorlagen** mindestens ein Basis-Dokument (z. B. Anschreiben, Lebenslauf)
hinterlegen — diese dienen der KI als Faktengrundlage.
- `OLLAMA_API_KEY` (Ollama Cloud) gesetzt z. B. in der `.env`-Datei des Trackers ,
damit die KI-Generierung läuft.
- Unter **Vorlagen** mindestens ein Basis-Dokument (z. B. Anschreiben,
Lebenslauf) hinterlegen — diese dienen der KI als Faktengrundlage.
- `OLLAMA_API_KEY` (Ollama Cloud) gesetzt z. B. in den Einstellungen des
Benutzers damit die KI-Generierung läuft.
- Ein **API-Token (X-API-Key)** für den benutzten Account.
## Ausgelesene Felder
## Berechtigungen
| Feld | Quelle (Indeed DOM) |
|------|---------------------|
| Stelle | `[data-testid="jobsearch-JobInfoHeader-title"]` |
| Firma | `[data-testid="inlineHeader-companyName"]` (im `jobsearch-CompanyInfoContainer`) |
| Ort | `[data-testid="jobsearch-JobInfoHeader-companyLocation"]` |
| Gehalt/Art | `#salaryInfoAndJobType` |
| Stellenbeschreibung | `#jobDescriptionText` |
| Quelle-URL | aus `vjk`/`jk`-Parameter bzw. aktueller URL |
| Berechtigung | Zweck |
|---|---|
| `storage` | API-Key speichern (`chrome.storage.sync`) |
| `scripting` + `activeTab` | Stelle auf der aktiven Seite auslesen (nur auf Klick) |
| `https://nextjobs.cc/*` | Import an den Tracker senden |
| `*://*.indeed.com/*` | Inline-Button auf Indeed einblenden |
Indeed ist eine Single-Page-App; die Erweiterung erkennt Wechsel der geöffneten
Stelle per `MutationObserver` und hält den Button aktuell.
Die Erweiterung liest nur die aktive Seite aus, wenn du sie aufrufst, und
sendet die Daten ausschließlich an `https://nextjobs.cc`.
+27 -16
View File
@@ -1,15 +1,18 @@
// Service worker: performs the cross-origin POST to the NextJobs.
// Routing the request through the background worker (instead of the content
// script) keeps it independent of the Indeed page's Content-Security-Policy.
// 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 DEFAULT_TRACKER_URL = 'http://localhost:3000';
const TRACKER_URL = 'https://nextjobs.cc';
function getTrackerUrl() {
function getApiKey() {
return new Promise((resolve) => {
chrome.storage.sync.get({ trackerUrl: DEFAULT_TRACKER_URL }, (items) => {
let url = (items.trackerUrl || DEFAULT_TRACKER_URL).trim();
url = url.replace(/\/+$/, ''); // strip trailing slashes
resolve(url);
chrome.storage.sync.get({ apiKey: '' }, (items) => {
resolve((items.apiKey || '').trim());
});
});
}
@@ -18,10 +21,14 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message && message.type === 'IMPORT_JOB') {
(async () => {
try {
const base = await getTrackerUrl();
const res = await fetch(base + '/api/indeed-import', {
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' },
headers: { 'Content-Type': 'application/json', 'X-API-Key': apiKey },
body: JSON.stringify(message.payload),
});
@@ -39,6 +46,10 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
});
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}`,
@@ -50,17 +61,17 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
ok: true,
id: data.id,
message: data.message,
openUrl: data.url ? base + data.url : null,
openUrl: data.url ? TRACKER_URL + data.url : null,
});
} catch (err) {
sendResponse({
ok: false,
error:
'Konnte den NextJobs nicht erreichen. Läuft er, und ist die ' +
'Adresse in den Erweiterungs-Einstellungen korrekt? (' + String(err.message) + ')',
'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
}
});
});
+5 -8
View File
@@ -1,15 +1,12 @@
{
"manifest_version": 3,
"name": "NextJobs Stellen-Import",
"version": "1.1.0",
"description": "Erfasst Stellen von jeder Webseite (nicht nur Indeed) über das Erweiterungs-Popup und sendet sie an den NextJobs, der automatisch zugeschnittene Bewerbungsunterlagen erstellt.",
"version": "2.0.0",
"description": "Erfasst Stellen von jeder Webseite (nicht nur Indeed) über das Erweiterungs-Popup und sendet sie an NextJobs (https://nextjobs.cc), der automatisch zugeschnittene Bewerbungsunterlagen erstellt. Authentifizierung über deinen persönlichen REST-API-Key.",
"permissions": ["storage", "scripting", "activeTab"],
"host_permissions": [
"*://*.indeed.com/*",
"http://localhost/*",
"http://127.0.0.1/*",
"http://*/*",
"https://*/*"
"https://nextjobs.cc/*",
"*://*.indeed.com/*"
],
"content_scripts": [
{
@@ -26,4 +23,4 @@
"default_popup": "popup.html",
"default_title": "NextJobs"
}
}
}
+4 -3
View File
@@ -94,10 +94,11 @@
<div id="status"></div>
<details id="settings">
<details id="settings" open>
<summary>Einstellungen</summary>
<label for="trackerUrl">Tracker-Adresse</label>
<input type="url" id="trackerUrl" placeholder="http://localhost:3000" />
<label for="apiKey">REST-API-Key</label>
<input type="password" id="apiKey" placeholder="API-Key aus den Tracker-Einstellungen" autocomplete="off" />
<p class="hint" style="margin:6px 0 0">Deinen persönlichen Key findest du im Tracker unter <b>Einstellungen → API-Token (X-API-Key)</b>. Pro Benutzer eigener Key.</p>
<div class="row">
<button class="primary" id="saveBtn">Speichern</button>
<button class="secondary" id="testBtn">Verbindung testen</button>
+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();