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:
@@ -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>.
|
||||
|
||||
Reference in New Issue
Block a user