diff --git a/app/app.js b/app/app.js index b5a0cb9..e118506 100644 --- a/app/app.js +++ b/app/app.js @@ -1,6 +1,6 @@ // ============================================================================ -// LAUNCHER — VERSIONE COMPLETA E OTTIMIZZATA (A) — BLOCCO 1/6 -// Sezione: Variabili globali + Storage + Config + Setup Page +// LAUNCHER — VERSIONE COMPLETA E OTTIMIZZATA (A) +// BLOCCO 1/6 — Variabili globali + Storage + Config + Setup Page // ============================================================================ // --------------------------------------------------------------------------- @@ -32,6 +32,7 @@ let dragOffsetX = 0; let dragOffsetY = 0; let dragStartX = 0; let dragStartY = 0; +let placeholderEl = null; // --------------------------------------------------------------------------- // CRITTOGRAFIA E STORAGE @@ -96,30 +97,18 @@ function loadApps() { } // --------------------------------------------------------------------------- -// SETUP PAGE (6 TAP PER APRIRE + AUTOCOMPILAZIONE) +// SETUP PAGE (6 TAP PER APRIRE + AUTOCOMPILAZIONE + "Aggiorna ora") // --------------------------------------------------------------------------- -/*function showSetupPage() { - const cfg = loadConfig(); - if (cfg) { - document.getElementById("cfg-url").value = cfg.url; - document.getElementById("cfg-user").value = cfg.user; - document.getElementById("cfg-pass").value = cfg.password; - } - document.getElementById("setup-page").classList.remove("hidden"); -}*/ - function showSetupPage() { const cfg = loadConfig(); if (cfg) { - // Popola i campi document.getElementById("cfg-url").value = cfg.url; document.getElementById("cfg-user").value = cfg.user; document.getElementById("cfg-pass").value = cfg.password; - // Mostra il pulsante "Aggiorna ora" + // Mostra il pulsante "Aggiorna ora" solo se esiste già una config document.getElementById("cfg-refresh").style.display = "block"; - } else { // Nessuna config → nascondi il pulsante document.getElementById("cfg-refresh").style.display = "none"; @@ -150,9 +139,10 @@ document.addEventListener("click", () => { showSetupPage(); } }); + // ============================================================================ -// LAUNCHER — VERSIONE COMPLETA E OTTIMIZZATA (A) — BLOCCO 2/6 -// Sezione: API login, getLinks, ordine apps, render, startLauncher +// LAUNCHER — VERSIONE COMPLETA E OTTIMIZZATA (A) +// BLOCCO 2/6 — API login, getLinks, ordine apps, render, startLauncher // ============================================================================ // --------------------------------------------------------------------------- @@ -280,7 +270,7 @@ function renderApps() { } // --------------------------------------------------------------------------- -// START LAUNCHER (carica locale → render → aggiorna server → init UI) +// START LAUNCHER (carica locale → render → init UI) // --------------------------------------------------------------------------- async function startLauncher() { @@ -288,7 +278,7 @@ async function startLauncher() { const saved = loadApps(); if (saved) { appsData = saved; - //console.log("Apps caricate da localStorage:", appsData); + console.log("Apps caricate da localStorage:", appsData); } // 2️⃣ Carica ordine @@ -297,24 +287,24 @@ async function startLauncher() { // 3️⃣ Render immediato (istantaneo) renderApps(); - // 4️⃣ Aggiorna in background dal server - //getLinks(); + // ❌ Nessun aggiornamento automatico dal server + // getLinks(); - // 5️⃣ Inizializza UI (zoom, drag, wiggle, menu…) + // 4️⃣ Inizializza UI (zoom, drag, wiggle, menu…) initZoomHandlers(); initLongPressHandlers(); initDragHandlers(); initContextMenuActions(); initGlobalCloseHandlers(); } + // ============================================================================ -// LAUNCHER — VERSIONE COMPLETA E OTTIMIZZATA (A) — BLOCCO 3/6 -// Sezione: Zoom stile iPhone (pinch, elasticità, wheel) +// LAUNCHER — VERSIONE COMPLETA E OTTIMIZZATA (A) +// BLOCCO 3/6 — Zoom stile iPhone (pinch, elasticità, wheel) // ============================================================================ // --------------------------------------------------------------------------- // Calcolo dinamico dello zoom massimo -// (dipende dalla larghezza dello schermo e dalla dimensione delle icone) // --------------------------------------------------------------------------- function computeDynamicMaxZoom() { return Math.min(window.innerWidth / 85, 4.0); @@ -370,7 +360,7 @@ function initZoomHandlers() { if (e.touches.length === 2) e.preventDefault(); }, { passive: false }); - // Inizio pinch o doppio tap + // Inizio pinch (NO double tap zoom) document.addEventListener("touchstart", e => { // Inizio pinch @@ -379,13 +369,7 @@ function initZoomHandlers() { if (zoomAnimFrame) cancelAnimationFrame(zoomAnimFrame); } - // Doppio tap → zoom rapido - /*const now = Date.now(); - if (e.touches.length === 1 && now - lastTapTime < 300) { - zoomMax = computeDynamicMaxZoom(); - applyZoom(Math.min(zoomLevel * 1.15, zoomMax)); - } - lastTapTime = now;*/ + // Nessuna azione sul doppio tap lastTapTime = Date.now(); }); @@ -449,9 +433,10 @@ function initZoomHandlers() { applyZoom(newZoom); }, { passive: false }); } + // ============================================================================ -// LAUNCHER — VERSIONE COMPLETA E OTTIMIZZATA (A) — BLOCCO 4/6 -// Sezione: Long‑press, Edit Mode, Context Menu, Global Close +// LAUNCHER — VERSIONE COMPLETA E OTTIMIZZATA (A) +// BLOCCO 4/6 — Long‑press, Edit Mode, Context Menu, Global Close // ============================================================================ // --------------------------------------------------------------------------- @@ -632,9 +617,10 @@ function initGlobalCloseHandlers() { } }); } + // ============================================================================ -// LAUNCHER — VERSIONE COMPLETA E OTTIMIZZATA (A) — BLOCCO 5/6 -// Sezione: Drag & Drop stile iPhone +// LAUNCHER — VERSIONE COMPLETA E OTTIMIZZATA (A) +// BLOCCO 5/6 — Drag & Drop stile iPhone (FIXED) // ============================================================================ // --------------------------------------------------------------------------- @@ -658,9 +644,11 @@ function getPointerPosition(e) { } // --------------------------------------------------------------------------- -// Inizio drag: crea icona flottante + placeholder invisibile +/* Inizio drag: icona flottante + placeholder nel layout */ // --------------------------------------------------------------------------- function startDrag(icon, pos) { + const folderEl = document.getElementById("folder"); + draggingId = icon.dataset.id; const r = icon.getBoundingClientRect(); @@ -678,20 +666,22 @@ function startDrag(icon, pos) { draggingIcon.style.pointerEvents = "none"; draggingIcon.style.transform = "translate3d(0,0,0)"; - // Placeholder invisibile che mantiene lo spazio - const placeholder = icon.cloneNode(true); - placeholder.classList.add("placeholder"); - placeholder.style.visibility = "hidden"; - icon.parentNode.insertBefore(placeholder, icon); + // Placeholder nel layout (slot vuoto) + placeholderEl = document.createElement("div"); + placeholderEl.className = "app-icon placeholder"; + placeholderEl.style.visibility = "hidden"; + + // Inserisci il placeholder dove stava l’icona + folderEl.insertBefore(placeholderEl, icon); hideContextMenu(); } // --------------------------------------------------------------------------- -// Aggiorna posizione dell’icona trascinata + reorder dinamico +// Aggiorna posizione icona trascinata + posizione placeholder // --------------------------------------------------------------------------- function updateDragPosition(pos) { - if (!draggingIcon) return; + if (!draggingIcon || !placeholderEl) return; const x = pos.pageX - dragOffsetX; const y = pos.pageY - dragOffsetY; @@ -699,63 +689,72 @@ function updateDragPosition(pos) { draggingIcon.style.left = `${x}px`; draggingIcon.style.top = `${y}px`; - const elem = document.elementFromPoint(pos.clientX, pos.clientY); - const targetIcon = elem && elem.closest(".app-icon:not(.dragging):not(.placeholder)"); - if (!targetIcon) return; + const centerX = pos.clientX; + const centerY = pos.clientY; - const from = appsOrder.indexOf(draggingId); - const to = appsOrder.indexOf(targetIcon.dataset.id); - if (from === -1 || to === -1 || from === to) return; + const elem = document.elementFromPoint(centerX, centerY); + const targetIcon = elem && elem.closest(".app-icon:not(.dragging)"); + if (!targetIcon || targetIcon === placeholderEl) return; - appsOrder.splice(from, 1); - appsOrder.splice(to, 0, draggingId); - saveOrder(); + const folderEl = document.getElementById("folder"); + const targetRect = targetIcon.getBoundingClientRect(); + const isBefore = centerY < targetRect.top + targetRect.height / 2; + + if (isBefore) { + folderEl.insertBefore(placeholderEl, targetIcon); + } else { + folderEl.insertBefore(placeholderEl, targetIcon.nextSibling); + } } // --------------------------------------------------------------------------- -// Fine drag: drop preciso nella cella corretta +// Fine drag: aggiorna appsOrder in base alla posizione del placeholder // --------------------------------------------------------------------------- function endDrag() { - if (!draggingIcon) return; + if (!draggingIcon || !placeholderEl) { + draggingIcon = null; + placeholderEl = null; + dragStartX = 0; + dragStartY = 0; + return; + } - const icon = draggingIcon; - draggingIcon = null; + const folderEl = document.getElementById("folder"); - // Rimuovi placeholder - const placeholder = document.querySelector(".app-icon.placeholder"); - if (placeholder) placeholder.remove(); + // Tutti i figli, inclusa la placeholder + const children = Array.from(folderEl.children); + const finalIndex = children.indexOf(placeholderEl); - // Calcola punto centrale dell’icona trascinata - const left = parseFloat(icon.style.left) || 0; - const top = parseFloat(icon.style.top) || 0; - const dropXClient = left + icon.offsetWidth / 2; - const dropYClient = top + icon.offsetHeight / 2; + // Ripristina icona visuale + draggingIcon.classList.remove("dragging"); + draggingIcon.style.position = ""; + draggingIcon.style.left = ""; + draggingIcon.style.top = ""; + draggingIcon.style.width = ""; + draggingIcon.style.height = ""; + draggingIcon.style.zIndex = ""; + draggingIcon.style.pointerEvents = ""; + draggingIcon.style.transform = ""; - const elem = document.elementFromPoint(dropXClient, dropYClient); - const targetIcon = elem && elem.closest(".app-icon:not(.dragging)"); - - if (targetIcon) { - const from = appsOrder.indexOf(icon.dataset.id); - const to = appsOrder.indexOf(targetIcon.dataset.id); - - if (from !== -1 && to !== -1 && from !== to) { - appsOrder.splice(from, 1); - appsOrder.splice(to, 0, icon.dataset.id); + if (finalIndex !== -1) { + const currentIndex = appsOrder.indexOf(draggingId); + if (currentIndex !== -1 && currentIndex !== finalIndex) { + appsOrder.splice(currentIndex, 1); + appsOrder.splice(finalIndex, 0, draggingId); saveOrder(); } } - // Ripristina icona - icon.classList.remove("dragging"); - icon.style.position = ""; - icon.style.left = ""; - icon.style.top = ""; - icon.style.width = ""; - icon.style.height = ""; - icon.style.zIndex = ""; - icon.style.pointerEvents = ""; - icon.style.transform = ""; + if (placeholderEl && placeholderEl.parentNode) { + placeholderEl.parentNode.removeChild(placeholderEl); + } + draggingIcon = null; + placeholderEl = null; + dragStartX = 0; + dragStartY = 0; + + // Ridisegna in base al nuovo ordine renderApps(); } @@ -764,9 +763,7 @@ function endDrag() { // --------------------------------------------------------------------------- function initDragHandlers() { - // --------------------------------------------------------- - // TOUCH DRAG - // --------------------------------------------------------- + // TOUCH DRAG document.addEventListener("touchstart", e => { if (!editMode) return; if (e.touches.length !== 1) return; @@ -788,7 +785,6 @@ function initDragHandlers() { const pos = getPointerPosition(e); - // Inizio drag if (!draggingIcon) { const dx = pos.clientX - dragStartX; const dy = pos.clientY - dragStartY; @@ -811,14 +807,17 @@ function initDragHandlers() { document.addEventListener("touchend", e => { if (!editMode) return; - if (draggingIcon && (!e.touches || e.touches.length === 0)) { + if (!draggingIcon) { + dragStartX = 0; + dragStartY = 0; + return; + } + if (!e.touches || e.touches.length === 0) { endDrag(); } }, { passive: true }); - // --------------------------------------------------------- - // MOUSE DRAG - // --------------------------------------------------------- + // MOUSE DRAG document.addEventListener("mousedown", e => { if (!editMode) return; if (e.button !== 0) return; @@ -862,54 +861,19 @@ function initDragHandlers() { document.addEventListener("mouseup", () => { if (!editMode) return; - dragStartX = 0; - dragStartY = 0; - if (draggingIcon) { - endDrag(); - } - draggingIcon = null; - draggingId = null; - }); - - document.addEventListener("mousemove", e => { - if (!editMode) return; - - const pos = getPointerPosition(e); - if (!draggingIcon) { - if (!dragStartX && !dragStartY) return; - - const dx = pos.clientX - dragStartX; - const dy = pos.clientY - dragStartY; - if (Math.hypot(dx, dy) > 10) { - const icon = e.target.closest(".app-icon"); - if (icon) { - if (longPressTimer) { - clearTimeout(longPressTimer); - longPressTimer = null; - longPressTarget = null; - } - startDrag(icon, pos); - } - } - } else { - updateDragPosition(pos); - } - }); - - document.addEventListener("mouseup", () => { - if (!editMode) return; - dragStartX = 0; - dragStartY = 0; - if (draggingIcon) { - endDrag(); + dragStartX = 0; + dragStartY = 0; + return; } + endDrag(); }); } -// ============================================================================ -// LAUNCHER — VERSIONE COMPLETA E OTTIMIZZATA (A) — BLOCCO 6/6 (FINALE) -// Sezione: Context Menu Actions + Config Save + Init Globale +// ============================================================================ +// LAUNCHER — VERSIONE COMPLETA E +// OTTIMIZZATA (A) BLOCCO 6/6 — Context Menu +// Actions + Config Save + Init Globale // ============================================================================ // --------------------------------------------------------------------------- @@ -970,19 +934,17 @@ function initContextMenuActions() { // --------------------------------------------------------------------------- document.getElementById("cfg-refresh").addEventListener("click", async () => { - // Carica config attuale const cfg = loadConfig(); if (!cfg) { alert("Config mancante. Inserisci URL, user e password."); return; } - // Aggiorna apps dal server const ok = await getLinks(); if (ok) { hideSetupPage(); - startLauncher(); // Ritorna subito alla schermata principale + startLauncher(); // Torna subito alla schermata principale } else { alert("Impossibile aggiornare le app dal server."); } @@ -996,16 +958,12 @@ document.getElementById("cfg-save").addEventListener("click", async () => { const user = document.getElementById("cfg-user").value; const pass = document.getElementById("cfg-pass").value; - // Salva configurazione saveConfig(url, user, pass); - // Scarica apps dal server const ok = await getLinks(); if (ok) { hideSetupPage(); - - // Restart completo del launcher startLauncher(); } }); @@ -1017,10 +975,8 @@ document.addEventListener("DOMContentLoaded", () => { const cfg = loadConfig(); if (!cfg) { - // Primo avvio → mostra setup showSetupPage(); } else { - // Config presente → avvia launcher hideSetupPage(); startLauncher(); } diff --git a/app/index.html b/app/index.html index f946b1d..ba09ee9 100644 --- a/app/index.html +++ b/app/index.html @@ -40,9 +40,9 @@ - --> + diff --git a/app/start.sh b/app/start.sh old mode 100755 new mode 100644 diff --git a/server/backend/appMetadata.js b/server/backend/appMetadata.js new file mode 100644 index 0000000..6f9cd45 --- /dev/null +++ b/server/backend/appMetadata.js @@ -0,0 +1,132 @@ +// appMetadata.js +import axios from "axios"; +import * as cheerio from "cheerio"; +import sizeOf from "image-size"; +import sharp from "sharp"; + +export async function getAppMetadata(baseUrl) { + console.log(baseUrl); + try { + const res = await axios.get(baseUrl, { timeout: 3000 }); + const $ = cheerio.load(res.data); + + // ------------------------------- + // 1. Nome più corto + // ------------------------------- + const nameCandidates = [ + $('meta[property="og:site_name"]').attr("content"), + $('meta[name="application-name"]').attr("content"), + $('meta[property="og:title"]').attr("content"), + $("title").text().trim() + ].filter(Boolean); + + const name = nameCandidates.length > 0 + ? nameCandidates.sort((a, b) => a.length - b.length)[0] + : "no_name"; + + // ------------------------------- + // 2. Icone HTML + // ------------------------------- + const htmlIcons = []; + $("link[rel*='icon']").each((_, el) => { + const href = $(el).attr("href"); + const sizes = $(el).attr("sizes") || ""; + if (href) htmlIcons.push({ href, sizes }); + }); + + // ------------------------------- + // 3. Manifest.json + // ------------------------------- + let manifestIcons = []; + const manifestHref = $('link[rel="manifest"]').attr("href"); + + if (manifestHref) { + try { + const manifestUrl = new URL(manifestHref, baseUrl).href; + const manifestRes = await axios.get(manifestUrl, { timeout: 3000 }); + const manifest = manifestRes.data; + + if (manifest.icons && Array.isArray(manifest.icons)) { + manifestIcons = manifest.icons.map(icon => ({ + href: icon.src, + sizes: icon.sizes || "" + })); + } + } catch {} + } + + // ------------------------------- + // 4. Fallback 4 icone + // ------------------------------- + const fallbackPaths = [ + "/favicon.ico", + "/favicon.png", + "/icon.png", + "/apple-touch-icon.png" + ]; + + const fallbackIcons = fallbackPaths.map(p => ({ + href: p, + sizes: "" + })); + + // ------------------------------- + // 5. Unisci tutte le icone + // ------------------------------- + const allIcons = [...htmlIcons, ...manifestIcons, ...fallbackIcons]; + + // ------------------------------- + // 6. Determina dimensione reale (PNG, ICO, SVG) + // ------------------------------- + const iconsWithRealSize = []; + + for (const icon of allIcons) { + try { + const url = new URL(icon.href, baseUrl).href; + const imgRes = await axios.get(url, { responseType: "arraybuffer" }); + + let width = 0; + + // ---- PNG / JPG / ICO ---- + try { + const dim = sizeOf(imgRes.data); + if (dim.width) width = dim.width; + } catch { + // Non è un formato supportato da image-size + } + + // ---- SVG → converti in PNG e misura ---- + if (width === 0) { + try { + const pngBuffer = await sharp(imgRes.data).png().toBuffer(); + const dim = sizeOf(pngBuffer); + if (dim.width) width = dim.width; + } catch { + // SVG non convertibile → ignora + } + } + + if (width > 0) { + iconsWithRealSize.push({ url, size: width }); + } + + } catch { + // icona non accessibile → ignora + } + } + + // ------------------------------- + // 7. Scegli la più grande + // ------------------------------- + iconsWithRealSize.sort((a, b) => b.size - a.size); + + const icon = iconsWithRealSize.length > 0 + ? iconsWithRealSize[0].url + : null; + + return { name, icon }; + + } catch { + return { name: "no_name", icon: null }; + } +} diff --git a/server/backend/index.js b/server/backend/index.js index c7a70a5..14b66a9 100644 --- a/server/backend/index.js +++ b/server/backend/index.js @@ -4,6 +4,7 @@ import cors from "cors"; import dotenv from "dotenv"; import linksRouter from "./routes/links.js"; import authRouter from "./routes/auth.js"; +import metadataRouter from "./routes/metadata.js"; dotenv.config(); @@ -21,6 +22,9 @@ app.use("/auth", authRouter); // Link routes (protette) app.use("/links", linksRouter); +// link per metadata +app.use("/metadata", metadataRouter); + // Connessione Mongo (URL da env con fallback) const MONGO_URI = process.env.MONGO_URI || "mongodb://mongo:27017/mydb"; diff --git a/server/backend/package-lock.json b/server/backend/package-lock.json index 129030a..4292898 100644 --- a/server/backend/package-lock.json +++ b/server/backend/package-lock.json @@ -1,17 +1,496 @@ { - "name": "server", + "name": "backend", "lockfileVersion": 3, "requires": true, "packages": { "": { "dependencies": { + "axios": "^1.13.2", "bcryptjs": "^3.0.3", + "cheerio": "^1.1.2", "cors": "^2.8.5", "dotenv": "^17.2.3", "express": "^5.2.1", + "image-size": "^2.0.2", "jsonwebtoken": "^9.0.3", "mongoose": "^9.0.2", - "multer": "^2.0.2" + "multer": "^2.0.2", + "sharp": "^0.34.5" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.1.tgz", + "integrity": "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@img/colour": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz", + "integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" } }, "node_modules/@mongodb-js/saslprep": { @@ -57,6 +536,23 @@ "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", "license": "MIT" }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", + "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/bcryptjs": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz", @@ -90,6 +586,12 @@ "url": "https://opencollective.com/express" } }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "license": "ISC" + }, "node_modules/bson": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/bson/-/bson-7.0.0.tgz", @@ -160,6 +662,60 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/cheerio": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.1.2.tgz", + "integrity": "sha512-IkxPpb5rS/d1IiLbHMgfPuS0FgiWTtFIm/Nj+2woXDLTZ7fOT2eqzgYbdMlLweqlHbsZjxEChoVK+7iph7jyQg==", + "license": "MIT", + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "encoding-sniffer": "^0.2.1", + "htmlparser2": "^10.0.0", + "parse5": "^7.3.0", + "parse5-htmlparser2-tree-adapter": "^7.1.0", + "parse5-parser-stream": "^7.1.2", + "undici": "^7.12.0", + "whatwg-mimetype": "^4.0.0" + }, + "engines": { + "node": ">=20.18.1" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/concat-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", @@ -228,6 +784,34 @@ "node": ">= 0.10" } }, + "node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -245,6 +829,15 @@ } } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -254,6 +847,70 @@ "node": ">= 0.8" } }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, "node_modules/dotenv": { "version": "17.2.3", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", @@ -304,6 +961,43 @@ "node": ">= 0.8" } }, + "node_modules/encoding-sniffer": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz", + "integrity": "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==", + "license": "MIT", + "dependencies": { + "iconv-lite": "^0.6.3", + "whatwg-encoding": "^3.1.1" + }, + "funding": { + "url": "https://github.com/fb55/encoding-sniffer?sponsor=1" + } + }, + "node_modules/encoding-sniffer/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -334,6 +1028,21 @@ "node": ">= 0.4" } }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -413,6 +1122,63 @@ "url": "https://opencollective.com/express" } }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/form-data/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/form-data/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -501,6 +1267,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -513,6 +1294,37 @@ "node": ">= 0.4" } }, + "node_modules/htmlparser2": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.0.0.tgz", + "integrity": "sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.1", + "entities": "^6.0.0" + } + }, + "node_modules/htmlparser2/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/http-errors": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", @@ -549,6 +1361,18 @@ "url": "https://opencollective.com/express" } }, + "node_modules/image-size": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-2.0.2.tgz", + "integrity": "sha512-IRqXKlaXwgSMAMtpNzZa1ZAe8m+Sa1770Dhk8VkSsP9LS+iHD62Zd8FQKs8fbPiagBE7BzoFX23cxFnwshpV6w==", + "license": "MIT", + "bin": { + "image-size": "bin/image-size.js" + }, + "engines": { + "node": ">=16.x" + } + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -920,6 +1744,18 @@ "node": ">= 0.6" } }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -962,6 +1798,55 @@ "wrappy": "1" } }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", + "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", + "license": "MIT", + "dependencies": { + "domhandler": "^5.0.3", + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-parser-stream": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", + "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==", + "license": "MIT", + "dependencies": { + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -994,6 +1879,12 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -1161,6 +2052,50 @@ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "license": "ISC" }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, "node_modules/side-channel": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", @@ -1295,6 +2230,13 @@ "node": ">=18" } }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, "node_modules/type-is": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", @@ -1315,6 +2257,15 @@ "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", "license": "MIT" }, + "node_modules/undici": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.16.0.tgz", + "integrity": "sha512-QEg3HPMll0o3t2ourKwOeUAZ159Kn9mx5pnzHRQO8+Wixmh88YdZRiIwat0iNzNNXn0yoEtXJqFpyW7eM8BV7g==", + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -1348,6 +2299,40 @@ "node": ">=12" } }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/whatwg-url": { "version": "14.2.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", diff --git a/server/backend/package.json b/server/backend/package.json index 5439c4e..d8c2ea9 100644 --- a/server/backend/package.json +++ b/server/backend/package.json @@ -1,13 +1,17 @@ { "type": "module", "dependencies": { + "axios": "^1.13.2", "bcryptjs": "^3.0.3", + "cheerio": "^1.1.2", "cors": "^2.8.5", "dotenv": "^17.2.3", "express": "^5.2.1", + "image-size": "^2.0.2", "jsonwebtoken": "^9.0.3", "mongoose": "^9.0.2", - "multer": "^2.0.2" + "multer": "^2.0.2", + "sharp": "^0.34.5" }, "scripts": { "start": "node index.js" diff --git a/server/backend/routes/links.js b/server/backend/routes/links.js index faec90a..f307553 100644 --- a/server/backend/routes/links.js +++ b/server/backend/routes/links.js @@ -1,11 +1,13 @@ import express from "express"; import multer from "multer"; +import axios from "axios"; +import fs from "fs"; +import path from "path"; import Link from "../models/Link.js"; import { authMiddleware } from "../middleware/auth.js"; const router = express.Router(); -// Config upload const storage = multer.diskStorage({ destination: "uploads/", filename: (req, file, cb) => { @@ -15,19 +17,46 @@ const storage = multer.diskStorage({ }); const upload = multer({ storage }); -// Tutte le rotte protette router.use(authMiddleware); -// GET /links - lista dei link dell'utente +async function downloadImage(url) { + const filename = Date.now() + ".png"; + const filepath = path.join("uploads", filename); + + const response = await axios({ + url, + method: "GET", + responseType: "stream", + headers: { "User-Agent": "Mozilla/5.0" } + }); + + await new Promise((resolve, reject) => { + const stream = response.data.pipe(fs.createWriteStream(filepath)); + stream.on("finish", resolve); + stream.on("error", reject); + }); + + return "/uploads/" + filename; +} + +function deleteOldIcon(iconPath) { + if (!iconPath) return; + const full = path.join(process.cwd(), iconPath.replace("/", "")); + fs.unlink(full, () => {}); +} + router.get("/", async (req, res) => { const links = await Link.find({ owner: req.userId }); res.json(links); }); -// POST /links - crea nuovo link con eventuale icona router.post("/", upload.single("icon"), async (req, res) => { - const { url, name } = req.body; - const iconPath = req.file ? `/uploads/${req.file.filename}` : null; + const { url, name, iconURL } = req.body; + + let iconPath = null; + + if (req.file) iconPath = `/uploads/${req.file.filename}`; + else if (iconURL) iconPath = await downloadImage(iconURL); const link = await Link.create({ url, @@ -39,39 +68,44 @@ router.post("/", upload.single("icon"), async (req, res) => { res.json(link); }); -// DELETE /links/:id -router.delete("/:id", async (req, res) => { - const { id } = req.params; - - const link = await Link.findOneAndDelete({ - _id: id, - owner: req.userId - }); - - if (!link) return res.status(404).json({ error: "Link non trovato" }); - - res.json({ success: true }); -}); - -// PUT /links/:id router.put("/:id", upload.single("icon"), async (req, res) => { const { id } = req.params; - const { name, url } = req.body; + const { name, url, iconURL } = req.body; - const update = {}; - if (name) update.name = name; - if (url) update.url = url; - if (req.file) update.icon = `/uploads/${req.file.filename}`; + const link = await Link.findOne({ _id: id, owner: req.userId }); + if (!link) return res.status(404).json({ error: "Link non trovato" }); - const link = await Link.findOneAndUpdate( + const update = { name, url }; + let newIcon = null; + + if (req.file) newIcon = `/uploads/${req.file.filename}`; + else if (iconURL) newIcon = await downloadImage(iconURL); + + if (newIcon) { + deleteOldIcon(link.icon); + update.icon = newIcon; + } + + const updated = await Link.findOneAndUpdate( { _id: id, owner: req.userId }, update, { new: true } ); + res.json(updated); +}); + +router.delete("/:id", async (req, res) => { + const link = await Link.findOneAndDelete({ + _id: req.params.id, + owner: req.userId + }); + if (!link) return res.status(404).json({ error: "Link non trovato" }); - res.json(link); + deleteOldIcon(link.icon); + + res.json({ success: true }); }); export default router; diff --git a/server/backend/routes/metadata.js b/server/backend/routes/metadata.js new file mode 100644 index 0000000..6fd61b3 --- /dev/null +++ b/server/backend/routes/metadata.js @@ -0,0 +1,104 @@ +import express from "express"; +import axios from "axios"; +import * as cheerio from "cheerio"; +import { URL } from "url"; + +const router = express.Router(); + +// Normalizza URL relativi → assoluti +function normalize(base, relative) { + try { + return new URL(relative, base).href; + } catch { + return null; + } +} + +// Scarica HTML con fallback CORS +async function fetchHTML(url) { + try { + const res = await axios.get(url, { + timeout: 8000, + headers: { + "User-Agent": "Mozilla/5.0" + } + }); + return res.data; + } catch (err) { + return null; + } +} + +router.get("/", async (req, res) => { + const siteUrl = req.query.url; + if (!siteUrl) return res.json({ error: "Missing URL" }); + + const html = await fetchHTML(siteUrl); + if (!html) return res.json({ name: null, icon: null }); + + const $ = cheerio.load(html); + + // ----------------------------------------- + // 1. Trova il nome più corto + // ----------------------------------------- + let names = []; + + const title = $("title").text().trim(); + if (title) names.push(title); + + $('meta[name="application-name"]').each((i, el) => { + const v = $(el).attr("content"); + if (v) names.push(v.trim()); + }); + + $('meta[property="og:site_name"]').each((i, el) => { + const v = $(el).attr("content"); + if (v) names.push(v.trim()); + }); + + const shortestName = names.length + ? names.sort((a, b) => a.length - b.length)[0] + : null; + + // ----------------------------------------- + // 2. Trova l’icona più grande + // ----------------------------------------- + let icons = []; + + $('link[rel="icon"], link[rel="shortcut icon"], link[rel="apple-touch-icon"], link[rel="apple-touch-icon-precomposed"]').each((i, el) => { + const href = $(el).attr("href"); + if (!href) return; + + const sizeAttr = $(el).attr("sizes"); + let size = 0; + + if (sizeAttr && sizeAttr.includes("x")) { + const parts = sizeAttr.split("x"); + size = parseInt(parts[0]) || 0; + } + + icons.push({ + url: normalize(siteUrl, href), + size + }); + }); + + // fallback favicon + icons.push({ + url: normalize(siteUrl, "/favicon.ico"), + size: 16 + }); + + // Ordina per dimensione + icons = icons.filter(i => i.url); + icons.sort((a, b) => b.size - a.size); + + const bestIcon = icons.length ? icons[0].url : null; + + res.json({ + name: shortestName, + icon: bestIcon + }); +}); + +export default router; diff --git a/server/backend/uploads/1767085843931-google.png b/server/backend/uploads/1767085843931-google.png deleted file mode 100644 index 1f90b36..0000000 Binary files a/server/backend/uploads/1767085843931-google.png and /dev/null differ diff --git a/server/backend/uploads/1767085872529-github.jpg b/server/backend/uploads/1767085872529-github.jpg deleted file mode 100644 index 476fbd4..0000000 Binary files a/server/backend/uploads/1767085872529-github.jpg and /dev/null differ diff --git a/server/backend/uploads/1767087426549-a.jpg b/server/backend/uploads/1767087426549-a.jpg deleted file mode 100644 index 8b960f2..0000000 Binary files a/server/backend/uploads/1767087426549-a.jpg and /dev/null differ diff --git a/server/backend/uploads/1767103637269-a.jpg b/server/backend/uploads/1767103637269-a.jpg deleted file mode 100644 index 8b960f2..0000000 Binary files a/server/backend/uploads/1767103637269-a.jpg and /dev/null differ diff --git a/server/backend/uploads/1767103690515-github.jpg b/server/backend/uploads/1767103690515-github.jpg deleted file mode 100644 index 476fbd4..0000000 Binary files a/server/backend/uploads/1767103690515-github.jpg and /dev/null differ diff --git a/server/backend/uploads/1767193346029-google.png b/server/backend/uploads/1767193346029-google.png deleted file mode 100644 index 3d6d694..0000000 Binary files a/server/backend/uploads/1767193346029-google.png and /dev/null differ diff --git a/server/backend/uploads/1767193354089-github.png b/server/backend/uploads/1767193354089-github.png deleted file mode 100644 index ee269b3..0000000 Binary files a/server/backend/uploads/1767193354089-github.png and /dev/null differ diff --git a/server/backend/uploads/1767193354094-github.png b/server/backend/uploads/1767193354094-github.png deleted file mode 100644 index ee269b3..0000000 Binary files a/server/backend/uploads/1767193354094-github.png and /dev/null differ diff --git a/server/frontend/api.js b/server/frontend/api.js index fd57756..c71723b 100644 --- a/server/frontend/api.js +++ b/server/frontend/api.js @@ -1,4 +1,4 @@ -const API_BASE = "http://192.168.1.3:3000"; +const API_BASE = "https://myapps_svr.patachina2.casacam.net"; // ------------------------------ // AUTH @@ -45,7 +45,7 @@ export async function getLinks(token) { return data; } -export async function createLink(token, { name, url, iconFile }) { +/*export async function createLink(token, { name, url, iconFile }) { const formData = new FormData(); formData.append("name", name); formData.append("url", url); @@ -59,6 +59,30 @@ export async function createLink(token, { name, url, iconFile }) { body: formData }); + const data = await res.json(); + if (!res.ok) throw new Error(data.error || "Errore creazione link"); + return data; +}*/ + +export async function createLink(token, { name, url, iconFile, iconURL }) { + const formData = new FormData(); + formData.append("name", name); + formData.append("url", url); + + if (iconFile) { + formData.append("icon", iconFile); + } + + if (iconURL) { + formData.append("iconURL", iconURL); + } + + const res = await fetch(`${API_BASE}/links`, { + method: "POST", + headers: { Authorization: `Bearer ${token}` }, + body: formData + }); + const data = await res.json(); if (!res.ok) throw new Error(data.error || "Errore creazione link"); return data; diff --git a/server/frontend/app.js b/server/frontend/app.js index 4362cea..653c298 100644 --- a/server/frontend/app.js +++ b/server/frontend/app.js @@ -7,78 +7,70 @@ import { updateLink } from "./api.js"; -const authSection = document.getElementById("authSection"); -const linkSection = document.getElementById("linkSection"); -const authStatus = document.getElementById("authStatus"); -const list = document.getElementById("list"); -const editModal = document.getElementById("editModal"); -const editForm = document.getElementById("editForm"); -const closeModal = document.getElementById("closeModal"); +const URL_SVR = "https://myapps_svr.patachina2.casacam.net"; -let editingId = null; let token = null; +let autoIconURL = null; +let editingId = null; -// ====================================================== +// =============================== +// MOSTRA DIMENSIONI IMMAGINE +// =============================== +function showImageSize(imgElement, sizeElement) { + const img = new Image(); + img.onload = () => { + sizeElement.textContent = `${img.width} × ${img.height} px`; + sizeElement.style.display = "block"; + }; + img.src = imgElement.src; +} + +// =============================== // AUTH -// ====================================================== - +// =============================== function setToken(t) { token = t; - - if (token) { - authSection.style.display = "none"; - linkSection.style.display = "block"; - loadLinks(); - } else { - authSection.style.display = "block"; - linkSection.style.display = "none"; - } + document.getElementById("authSection").style.display = token ? "none" : "block"; + document.getElementById("linkSection").style.display = token ? "block" : "none"; + if (token) loadLinks(); } document.getElementById("loginForm").addEventListener("submit", async e => { e.preventDefault(); - const email = e.target.email.value; - const password = e.target.password.value; - try { - const t = await login(email, password); + const t = await login(e.target.email.value, e.target.password.value); setToken(t); } catch (err) { - authStatus.textContent = err.message; + document.getElementById("authStatus").textContent = err.message; } }); document.getElementById("registerForm").addEventListener("submit", async e => { e.preventDefault(); - const email = e.target.email.value; - const password = e.target.password.value; - try { - await register(email, password); - authStatus.textContent = "Registrato! Ora effettua il login."; + await register(e.target.email.value, e.target.password.value); + document.getElementById("authStatus").textContent = "Registrato! Ora accedi."; } catch (err) { - authStatus.textContent = err.message; + document.getElementById("authStatus").textContent = err.message; } }); -// ====================================================== -// LINKS -// ====================================================== - +// =============================== +// LOAD LINKS +// =============================== async function loadLinks() { const links = await getLinks(token); + const list = document.getElementById("list"); list.innerHTML = links .map( link => `
- ${link.icon ? `` : ""} - + ${link.icon ? `` : ""}
${link.name}
${link.url}
-
@@ -89,103 +81,144 @@ async function loadLinks() { .join(""); } -// ====================================================== -// CREAZIONE LINK -// ====================================================== +// =============================== +// METADATA (icona automatica) +// =============================== +document.getElementById("fetchMetaBtn").addEventListener("click", async () => { + const url = document.getElementById("urlInput").value.trim(); + if (!url) return; + const res = await fetch(`${URL_SVR}/metadata?url=${encodeURIComponent(url)}`); + const data = await res.json(); + + document.getElementById("nameInput").value = data.name || ""; + autoIconURL = data.icon || null; + + // L’icona automatica è l’ultima scelta → reset input manuale + const fileInput = document.getElementById("iconInput"); + fileInput.value = ""; + + const preview = document.getElementById("iconPreview"); + const sizeBox = document.getElementById("iconSize"); + + if (autoIconURL) { + preview.src = autoIconURL; + preview.style.display = "block"; + sizeBox.style.display = "none"; + showImageSize(preview, sizeBox); + } +}); + +// =============================== +// ANTEPRIMA ICONA MANUALE +// =============================== +document.getElementById("iconInput").addEventListener("change", e => { + const file = e.target.files[0]; + if (!file) return; + + autoIconURL = null; // manuale vince + + const preview = document.getElementById("iconPreview"); + const sizeBox = document.getElementById("iconSize"); + + preview.src = URL.createObjectURL(file); + preview.style.display = "block"; + sizeBox.style.display = "none"; + + showImageSize(preview, sizeBox); +}); + +// =============================== +// CREAZIONE LINK +// =============================== document.getElementById("linkForm").addEventListener("submit", async e => { e.preventDefault(); - const formData = new FormData(e.target); - const iconFile = formData.get("icon"); + const raw = new FormData(e.target); + const manualFile = raw.get("icon"); + const hasManualFile = manualFile instanceof File && manualFile.size > 0; await createLink(token, { - name: formData.get("name"), - url: formData.get("url"), - iconFile: iconFile.size > 0 ? iconFile : null + name: raw.get("name"), + url: raw.get("url"), + iconFile: hasManualFile ? manualFile : null, + iconURL: !hasManualFile ? autoIconURL : null }); + autoIconURL = null; + document.getElementById("iconPreview").style.display = "none"; + document.getElementById("iconSize").style.display = "none"; + e.target.reset(); loadLinks(); }); -// ====================================================== -// AZIONI: MODIFICA + ELIMINA -// ====================================================== - -list.addEventListener("click", async e => { +// =============================== +// EDIT +// =============================== +document.getElementById("list").addEventListener("click", e => { const id = e.target.dataset.id; if (!id) return; - // ------------------------- - // ELIMINA - // ------------------------- if (e.target.classList.contains("deleteBtn")) { - if (confirm("Vuoi davvero eliminare questo link?")) { - await deleteLink(token, id); - loadLinks(); - } + deleteLink(token, id).then(loadLinks); return; } - // ------------------------- - // MODIFICA - // ------------------------- -/* if (e.target.classList.contains("editBtn")) { - const newName = prompt("Nuovo nome:"); - const newUrl = prompt("Nuovo URL:"); + if (e.target.classList.contains("editBtn")) { + editingId = id; - if (!newName && !newUrl) return; + const item = e.target.closest(".item"); + const name = item.querySelector("strong").textContent; + const url = item.querySelector("a").textContent; - await updateLink(token, id, { - name: newName, - url: newUrl - }); + const form = document.getElementById("editForm"); + form.name.value = name; + form.url.value = url; - loadLinks(); - }*/ + document.getElementById("iconPreviewEdit").style.display = "none"; + document.getElementById("iconSizeEdit").style.display = "none"; -if (e.target.classList.contains("editBtn")) { - const id = e.target.dataset.id; - editingId = id; - - // Precompila i campi - const item = e.target.closest(".item"); - const name = item.querySelector("strong").textContent; - const url = item.querySelector("a").textContent; - - editForm.name.value = name; - editForm.url.value = url; - editForm.icon.value = ""; // reset file input - - editModal.style.display = "flex"; -} - -closeModal.addEventListener("click", () => { - editModal.style.display = "none"; + document.getElementById("editModal").style.display = "flex"; + } }); -editForm.addEventListener("submit", async e => { +// ANTEPRIMA MANUALE IN EDIT +document.getElementById("iconInputEdit").addEventListener("change", e => { + const file = e.target.files[0]; + if (!file) return; + + const preview = document.getElementById("iconPreviewEdit"); + const sizeBox = document.getElementById("iconSizeEdit"); + + preview.src = URL.createObjectURL(file); + preview.style.display = "block"; + sizeBox.style.display = "none"; + + showImageSize(preview, sizeBox); +}); + +// SALVA EDIT +document.getElementById("editForm").addEventListener("submit", async e => { e.preventDefault(); - const name = editForm.name.value; - const url = editForm.url.value; - const iconFile = editForm.icon.files[0] || null; + const name = e.target.name.value; + const url = e.target.url.value; + const iconFile = e.target.icon.files[0] || null; await updateLink(token, editingId, { name, url, - iconFile + iconFile, + iconURL: null }); - editModal.style.display = "none"; + document.getElementById("editModal").style.display = "none"; loadLinks(); }); +document.getElementById("closeModal").addEventListener("click", () => { + document.getElementById("editModal").style.display = "none"; }); -// ====================================================== -// INIT -// ====================================================== - setToken(null); diff --git a/server/frontend/getAppMetadata.js b/server/frontend/getAppMetadata.js new file mode 100644 index 0000000..2cc7c5d --- /dev/null +++ b/server/frontend/getAppMetadata.js @@ -0,0 +1,83 @@ + diff --git a/server/frontend/index.html b/server/frontend/index.html index b9669d8..0fbdb07 100644 --- a/server/frontend/index.html +++ b/server/frontend/index.html @@ -37,9 +37,21 @@

Nuovo link

- - - + +
+ + +
+ + + + + + + + + +
@@ -53,19 +65,28 @@
-