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
+53 -1
View File
@@ -50,6 +50,7 @@ const migrate = require('./lib/migrate-multiuser');
const { runMigration, importEnvIntoAdmin } = migrate;
const suchprofil = require('./lib/suchprofil');
const suche = require('./lib/suche');
const { zipDir } = require('./lib/zipdir');
const app = express();
const PORT = process.env.PORT || 3000;
@@ -216,6 +217,7 @@ function isPublicPath(p) {
if (PUBLIC_PATHS.has(p)) return true;
if (p === '/health' || p === '/swagger' || p === '/swagger.json' || p === '/api-docs') return true;
if (p.startsWith('/api/v1/')) return true; // own X-API-Key auth
if (p === '/api/indeed-import') return true; // X-API-Key auth (Browser-Plugin)
return false;
}
@@ -258,6 +260,38 @@ function loginOk(req) {
loginFails.delete(clientIp(req));
}
// X-API-Key-Authentifizierung für Endpunkte, die ohne Browser-Session von außen
// kommen (heute: das Browser-Plugin über /api/indeed-import). Löst den Header
// X-API-Key zum Benutzer auf, dessen cfg:API_TOKEN ihn trägt, und läuft den
// Request in dessen Kontext — identisch zu /api/v1 (siehe lib/api.js). Ein
// fehlender oder unbekannter Key → 401; ein mehrdeutiger Token → fail closed.
async function requireApiKey(req, res, next) {
const provided = req.get('X-API-Key');
if (!provided) {
return res.status(401).json({ error: 'Ungültiger oder fehlender API-Key (Header: X-API-Key).' });
}
try {
const matches = await dbAll(
`SELECT u.id, u.username, u.is_admin
FROM app_state a JOIN users u ON u.id = a.user_id
WHERE a.key = ? AND a.value = ? AND a.value != ''`,
['cfg:API_TOKEN', provided]
);
if (matches.length !== 1) {
if (matches.length > 1) console.error('Plugin-Auth: Token ist mehreren Benutzern zugeordnet — Zugriff verweigert.');
return res.status(401).json({ error: 'Ungültiger oder fehlender API-Key (Header: X-API-Key).' });
}
const user = matches[0];
req.user = user;
req.apiUser = user; // kenntzeichnet API- (ggf. nicht Admin-) Kontext
await config.ensureLoaded(user.id);
userContext.run(user, next);
} catch (err) {
console.error('Plugin-API-Auth-Fehler:', err);
res.status(401).json({ error: 'Ungültiger oder fehlender API-Key (Header: X-API-Key).' });
}
}
// Login page + form handler.
app.get('/login', (req, res) => {
if (req.user) return res.redirect('/');
@@ -2294,7 +2328,7 @@ initializeDatabase().then(async () => {
});
// ----- Indeed import (called by the browser extension) -----
app.post('/api/indeed-import', async (req, res) => {
app.post('/api/indeed-import', requireApiKey, async (req, res) => {
try {
const { firma, stelle, ort, gehalt, stellenbeschreibung, quelle_url, art } = req.body || {};
@@ -2357,6 +2391,24 @@ initializeDatabase().then(async () => {
}
});
// ----- Browser-Plugin als ZIP herunterladen -----
// Packt das Verzeichnis extension/ on the fly (STORE, dep-frei). Liegt hinter
// requireAuth — nur angemeldete Nutzer laden das Plugin. Der Dateiname ist
// stabil, das Archiv deterministisch (fester Zeitstempel) → cache-freundlich.
app.get('/plugin/nextjobs-stellen-import.zip', (req, res) => {
try {
const dir = path.join(__dirname, 'extension');
const zip = zipDir(dir);
res.setHeader('Content-Type', 'application/zip');
res.setHeader('Content-Disposition', 'attachment; filename="nextjobs-stellen-import.zip"');
res.setHeader('Content-Length', zip.length);
res.send(zip);
} catch (error) {
console.error('Plugin-Download:', error);
res.status(500).send('Plugin konnte nicht gepackt werden.');
}
});
// Volltextsuche für die Startseite: Bewerbungen samt ihrer E-Mail-Korrespondenz
// (Absender, Betreff, Text), Notizen und Stellenbeschreibung. Liefert je Treffer
// die Fundstellen mit Snippet — die Marker darin ersetzt der Client durch <mark>.