@@ -829,16 +829,17 @@ router.post('/run-daily-check', authenticate, async (req, res) => {
} catch ( e ) { res . status ( 500 ) . json ( { error : e . message } ) ; }
} ) ;
// ── Öffentlicher Teilen-Link (kein Login nötig) ─────────────────────────────
// ── Öffentliche Teilen-Links (kein Login nötig) ─────────────────────────────
// Sicherheitsprinzip: die öffentlichen Routen unten liefern AUSSCHLIESSLICH
// bereits gecachte Daten aus (kein forceRefresh, kein Puppeteer-Trigger, kein
// Zugriff auf irgendeine Admin-Funktion) und sind zusätzlich pro IP
// ratenbegrenzt — ein gefundener/erratener Link kann also weder einen echten
// marktguru-Abruf erzwingen noch den Server sonst irgendwie belasten.
const KOEPI _SHARE _TOKEN _KEY = 'koepi_share_token' ;
function getShareToken ( ) {
return db . prepare ( 'SELECT value FROM admin_settings WHERE key=?' ) . get ( KOEPI _SHARE _TOKEN _KEY ) ? . value || null ;
//
// Mehrere benannte Links gleichzeitig möglich (z.B. einer pro Person), damit
// in den Logs nachvollziehbar ist, WER welchen Link benutzt hat.
function getShareLinkByToken ( token ) {
return db . prepare ( 'SELECT id, name FROM koepi_share_links WHERE token=?' ) . get ( token ) || null ;
}
// Einfache In-Memory-Ratenbegrenzung pro IP (30 Anfragen/Minute) — bewusst
@@ -863,26 +864,40 @@ setInterval(() => {
}
} , 5 * 60 * 1000 ) ;
// GET /share-link – aktuell en Link anzeig en (Admin)
router . get ( '/share-link' , authenticate , ( req , res ) => {
// GET /share-links – alle benannt en Links auflist en (Admin)
router . get ( '/share-links ' , authenticate , ( req , res ) => {
if ( req . user ? . role !== 'admin' ) return res . status ( 403 ) . json ( { error : 'Kein Zugriff' } ) ;
const token = getShareToken ( ) ;
res . json ( { token , url : token ? ` ${ req . protocol } :// ${ req . get ( 'host' ) } /kp/ ${ token } ` : null } ) ;
const links = db . prepare ( 'SELECT id, token, name, created_at FROM koepi_share_links ORDER BY created_at DESC' ) . all ( )
. map ( l => ( { ... l , url : ` ${ req . protocol } :// ${ req . get ( 'host' ) } /kp/ ${ l . token} ` }) ) ;
res . json ( { links } ) ;
} ) ;
// POST /share-link – neuen Link erzeugen (macht einen evtl. vorhandenen alten
// automatisch ungültig, da nur ein Token gleichzeitig gespeichert wird)
router . post ( '/share-link' , authenticate , ( req , res ) => {
// POST /share-links – neuen benannten Link erstellen (Admin)
router . post ( '/share-links' , authenticate , ( req , res ) => {
if ( req . user ? . role !== 'admin' ) return res . status ( 403 ) . json ( { error : 'Kein Zugriff' } ) ;
const name = ( req . body ? . name || '' ) . trim ( ) ;
if ( ! name ) return res . status ( 400 ) . json ( { error : 'Name fehlt' } ) ;
const token = crypto . randomBytes ( 20 ) . toString ( 'hex' ) ;
db . prepare ( ' INSERT OR REPLACE INTO admin_sett ing s (key , value) VALUES (?, ?)' ) . run ( KOEPI _SHARE _TOKEN _KEY , token ) ;
res . json ( { token , url : ` ${ req . protocol } :// ${ req . get ( 'host' ) } /kp/ ${ token } ` } ) ;
db . prepare ( ` INSERT INTO koepi_share_l ink s (to ken , name, created_at) VALUES (?, ?, datetime('now','localtime')) ` ) . run ( token , name ) ;
const link = db . prepare ( 'SELECT id, token, name, created_at FROM koepi_share_links WHERE token=?' ) . get ( token ) ;
res . json ( { ... link , url : ` ${ req . protocol } :// ${ req . get ( 'host' ) } /kp/ ${ token } ` } ) ;
} ) ;
// POST /share-link/disable – Link deaktivieren
router . post ( '/share-link/disable' , authenticate , ( req , res ) => {
// POST /share-links/:id/reset – Token dieses Links neu erzeugen (Name bleibt,
// alter Link wird ungültig)
router . post ( '/share-links/:id/reset' , authenticate , ( req , res ) => {
if ( req . user ? . role !== 'admin' ) return res . status ( 403 ) . json ( { error : 'Kein Zugriff' } ) ;
db . prepare ( 'D ELETE FROM admin_sett ing s WHERE key =?' ) . run ( KOEPI _SHARE _TOKEN _KEY ) ;
const existing = db . prepare ( 'S ELECT * FROM koepi_share_l ink s WHERE id =?' ) . get ( req . params . id ) ;
if ( ! existing ) return res . status ( 404 ) . json ( { error : 'Nicht gefunden' } ) ;
const token = crypto . randomBytes ( 20 ) . toString ( 'hex' ) ;
db . prepare ( 'UPDATE koepi_share_links SET token=? WHERE id=?' ) . run ( token , req . params . id ) ;
res . json ( { id : existing . id , token , name : existing . name , url : ` ${ req . protocol } :// ${ req . get ( 'host' ) } /kp/ ${ token } ` } ) ;
} ) ;
// DELETE /share-links/:id – Link löschen (Admin)
router . delete ( '/share-links/:id' , authenticate , ( req , res ) => {
if ( req . user ? . role !== 'admin' ) return res . status ( 403 ) . json ( { error : 'Kein Zugriff' } ) ;
db . prepare ( 'DELETE FROM koepi_share_links WHERE id=?' ) . run ( req . params . id ) ;
res . json ( { ok : true } ) ;
} ) ;
@@ -891,8 +906,8 @@ router.post('/share-link/disable', authenticate, (req, res) => {
// damit die installierte App direkt wieder auf der öffentlichen Seite landet
// statt auf dem normalen Login der Haupt-App.
router . get ( '/public/:token/manifest.json' , ( req , res ) => {
const active = getShareToken ( ) ;
if ( ! active || req . params . token !== active ) return res . status ( 404 ) . json ( { error : 'Nicht gefunden' } ) ;
const link = getShareLinkBy Token ( req . params . token ) ;
if ( ! link ) return res . status ( 404 ) . json ( { error : 'Nicht gefunden' } ) ;
res . setHeader ( 'Content-Type' , 'application/manifest+json' ) ;
res . json ( {
name : 'Dickens Prospekte' ,
@@ -914,8 +929,8 @@ router.get('/public/:token/manifest.json', (req, res) => {
// GET /public/:token/offers – öffentlich, nur Cache, kein Login
router . get ( '/public/:token/offers' , publicRateLimit , async ( req , res ) => {
const active = getShareToken ( ) ;
if ( ! active || req . params . token !== active ) return res . status ( 404 ) . json ( { error : 'Nicht gefunden' } ) ;
const link = getShareLinkBy Token ( req . params . token ) ;
if ( ! link ) return res . status ( 404 ) . json ( { error : 'Nicht gefunden' } ) ;
try {
const result = await scrapeMarktguru ( false ) ; // niemals forceRefresh über die öffentliche Route
res . json ( result ) ;
@@ -924,9 +939,9 @@ router.get('/public/:token/offers', publicRateLimit, async (req, res) => {
// GET /public/:token/prospekte – öffentlich, nur Cache, kein Login
router . get ( '/public/:token/prospekte' , publicRateLimit , async ( req , res ) => {
const active = getShareToken ( ) ;
if ( ! active || req . params . token !== active ) return res . status ( 404 ) . json ( { error : 'Nicht gefunden' } ) ;
logPublicAccess ( { linkType : 'koepi_share' , path : ` /kp/ ${ req . params . token } ` , ip : req . ip , userAgent : req . headers [ 'user-agent' ] } ) ;
const link = getShareLinkBy Token ( req . params . token ) ;
if ( ! link ) return res . status ( 404 ) . json ( { error : 'Nicht gefunden' } ) ;
logPublicAccess ( { linkType : 'koepi_share' , path : ` /kp/ ${ req . params . token } ` , ip : req . ip , userAgent : req . headers [ 'user-agent' ] , linkName : link . name } ) ;
try {
const result = await scrapeProspekte ( ) ;
res . json ( result ) ;