Egosurfing: búsqueda real de huella digital con resultados top 5
Backend: - Nuevo route GET /api/egosearch con rate limit (8 req/min) - Usa Google Custom Search API si GOOGLE_API_KEY+CSE_ID configurados - Fallback a instancias públicas SearXNG con JSON API (sin API key) - Devuelve top 5: title, url, snippet, domain, engine Frontend egosurfing.html: - Barra de búsqueda prominente con 5 modos (nombre/email/usuario/teléfono/libre) - Resultados en cards: dominio, título, snippet, acciones (ver, RTBF, GDPR) - RTBF link contextual según el dominio del resultado - Google dorking rápido: plantillas con 1 clic que se lanzan al buscador - Herramientas complementarias: HIBP, TinEye, WhatsMyName, formularios RTBF Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
7507b3b8e3
commit
f1d80dde13
4 changed files with 728 additions and 392 deletions
|
|
@ -14,3 +14,10 @@ GOOGLE_REDIRECT_URI=https://resetea.net/api/gmail/callback
|
||||||
|
|
||||||
# Puerto del servidor (por defecto 8787)
|
# Puerto del servidor (por defecto 8787)
|
||||||
PORT=8787
|
PORT=8787
|
||||||
|
|
||||||
|
# Google Custom Search (egosurfing — opcional, mejora la calidad de resultados)
|
||||||
|
# 1. Ve a https://programmablesearchengine.google.com/ → crear motor
|
||||||
|
# 2. Activa "Buscar en toda la web" y copia el ID (cx)
|
||||||
|
# 3. En https://console.cloud.google.com/ activa "Custom Search API" y crea una API Key
|
||||||
|
GOOGLE_API_KEY=
|
||||||
|
GOOGLE_CSE_ID=
|
||||||
|
|
|
||||||
19
api/app.js
19
api/app.js
|
|
@ -6,8 +6,9 @@ const express = require('express');
|
||||||
const helmet = require('helmet');
|
const helmet = require('helmet');
|
||||||
const rateLimit = require('express-rate-limit');
|
const rateLimit = require('express-rate-limit');
|
||||||
|
|
||||||
const eraseRoute = require('./routes/erase');
|
const eraseRoute = require('./routes/erase');
|
||||||
const gmailOAuth = require('./routes/gmail_oauth');
|
const gmailOAuth = require('./routes/gmail_oauth');
|
||||||
|
const egosearch = require('./routes/egosearch');
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
|
|
@ -30,10 +31,18 @@ const emailLimit = rateLimit({
|
||||||
message: { error: 'Demasiadas solicitudes de envío. Espera antes de reintentar.' }
|
message: { error: 'Demasiadas solicitudes de envío. Espera antes de reintentar.' }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── Rate limits específicos ───────────────────────────────────────
|
||||||
|
const searchLimit = rateLimit({
|
||||||
|
windowMs: 60 * 1000, // 1 minuto
|
||||||
|
max: 8,
|
||||||
|
message: { error: 'Demasiadas búsquedas. Espera un momento.' }
|
||||||
|
});
|
||||||
|
|
||||||
// ── Rutas ────────────────────────────────────────────────────────
|
// ── Rutas ────────────────────────────────────────────────────────
|
||||||
app.post('/api/erase', emailLimit, eraseRoute);
|
app.post('/api/erase', emailLimit, eraseRoute);
|
||||||
app.get('/api/gmail/auth', emailLimit, gmailOAuth.authInit);
|
app.get('/api/egosearch', searchLimit, egosearch);
|
||||||
app.get('/api/gmail/callback', gmailOAuth.authCallback);
|
app.get('/api/gmail/auth', emailLimit, gmailOAuth.authInit);
|
||||||
|
app.get('/api/gmail/callback', gmailOAuth.authCallback);
|
||||||
|
|
||||||
// Health check
|
// Health check
|
||||||
app.get('/api/health', (req, res) => res.json({ status: 'ok' }));
|
app.get('/api/health', (req, res) => res.json({ status: 'ok' }));
|
||||||
|
|
|
||||||
107
api/routes/egosearch.js
Normal file
107
api/routes/egosearch.js
Normal file
|
|
@ -0,0 +1,107 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
/* ── Instancias públicas SearXNG con JSON API (fallback si no hay Google CSE) ── */
|
||||||
|
const SEARX_INSTANCES = [
|
||||||
|
'https://searx.be',
|
||||||
|
'https://priv.au',
|
||||||
|
'https://search.mdosch.de',
|
||||||
|
'https://searxng.site',
|
||||||
|
];
|
||||||
|
|
||||||
|
async function searchSearX(instance, query) {
|
||||||
|
const url = `${instance}/search?` + new URLSearchParams({
|
||||||
|
q: query,
|
||||||
|
format: 'json',
|
||||||
|
categories: 'general',
|
||||||
|
language: 'es-ES',
|
||||||
|
});
|
||||||
|
|
||||||
|
const res = await fetch(url, {
|
||||||
|
headers: {
|
||||||
|
'User-Agent': 'resetea.net/1.0 (egosurfing privacy tool)',
|
||||||
|
'Accept': 'application/json',
|
||||||
|
},
|
||||||
|
signal: AbortSignal.timeout(7000),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||||
|
const data = await res.json();
|
||||||
|
return (data.results || []).map(r => ({
|
||||||
|
title: r.title || '',
|
||||||
|
url: r.url || '',
|
||||||
|
snippet: r.content || '',
|
||||||
|
engine: r.engine || 'web',
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function searchGoogle(query) {
|
||||||
|
const url = 'https://www.googleapis.com/customsearch/v1?' + new URLSearchParams({
|
||||||
|
key: process.env.GOOGLE_API_KEY,
|
||||||
|
cx: process.env.GOOGLE_CSE_ID,
|
||||||
|
q: query,
|
||||||
|
num: 10,
|
||||||
|
hl: 'es',
|
||||||
|
});
|
||||||
|
|
||||||
|
const res = await fetch(url, { signal: AbortSignal.timeout(7000) });
|
||||||
|
if (!res.ok) {
|
||||||
|
const err = await res.json().catch(() => ({}));
|
||||||
|
throw new Error(err.error?.message || `HTTP ${res.status}`);
|
||||||
|
}
|
||||||
|
const data = await res.json();
|
||||||
|
return (data.items || []).map(item => ({
|
||||||
|
title: item.title || '',
|
||||||
|
url: item.link || '',
|
||||||
|
snippet: item.snippet || '',
|
||||||
|
engine: 'google',
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function domainOf(rawUrl) {
|
||||||
|
try { return new URL(rawUrl).hostname.replace(/^www\./, ''); }
|
||||||
|
catch { return ''; }
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = async (req, res) => {
|
||||||
|
const { q } = req.query;
|
||||||
|
|
||||||
|
if (!q || q.trim().length < 2)
|
||||||
|
return res.status(400).json({ error: 'Introduce al menos 2 caracteres.' });
|
||||||
|
|
||||||
|
if (q.trim().length > 300)
|
||||||
|
return res.status(400).json({ error: 'Búsqueda demasiado larga.' });
|
||||||
|
|
||||||
|
/* Si la query no tiene operadores especiales la ponemos entre comillas
|
||||||
|
para forzar coincidencia exacta (ideal para nombre/email/alias). */
|
||||||
|
const query = /[:"()]/.test(q) ? q.trim() : `"${q.trim()}"`;
|
||||||
|
|
||||||
|
let raw = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (process.env.GOOGLE_API_KEY && process.env.GOOGLE_CSE_ID) {
|
||||||
|
raw = await searchGoogle(query);
|
||||||
|
} else {
|
||||||
|
for (const inst of SEARX_INSTANCES) {
|
||||||
|
try {
|
||||||
|
raw = await searchSearX(inst, query);
|
||||||
|
if (raw.length) break;
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(`[egosearch] ${inst} falló:`, e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[egosearch] error final:', e.message);
|
||||||
|
return res.status(502).json({ error: 'El servicio de búsqueda no está disponible ahora. Inténtalo de nuevo.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = raw.slice(0, 5).map(r => ({
|
||||||
|
title: r.title,
|
||||||
|
url: r.url,
|
||||||
|
snippet: r.snippet,
|
||||||
|
domain: domainOf(r.url),
|
||||||
|
engine: r.engine,
|
||||||
|
}));
|
||||||
|
|
||||||
|
res.json({ results, query, total: raw.length });
|
||||||
|
};
|
||||||
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue