commit 7560875884167ec1f65ae3ee8f22a01cc249f058 Author: Fabio Date: Mon Jan 5 22:50:31 2026 +0100 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ab81324 --- /dev/null +++ b/.gitignore @@ -0,0 +1,41 @@ +# Node +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + +# Build output +dist/ +build/ +out/ +.tmp/ +.temp/ + +# Capacitor / Cordova +android/ +ios/ +www/ + +# Environment files +#.env +#.env.* +#!.env.example + +# System files +.DS_Store +Thumbs.db + +# Editor folders +.vscode/ +.idea/ + +# Logs +*.log + +# Cache +.cache/ +.parcel-cache/ +.next/ +.nuxt/ +.svelte-kit/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..5224d73 --- /dev/null +++ b/README.md @@ -0,0 +1,76 @@ +# App per vedere tutte le mie app in un unica schermata + +si compone di + +- un server che condivide la lista delle apps + fa utilizzo di un server già attivo mongoDB +- la UI del server + che permette di inserire e modificare tutti i dati delle apps +- una app che funziona sia su smartphone che su PC + +## Server + +andare in + + cd server/backend + +installare + + npm ci install + +editare .env + +```sh +# === SERVER CONFIG === +PORT=3000 + +# === JWT CONFIG === +# Cambialo SEMPRE in produzione +JWT_SECRET=master66 + +# === MONGO CONFIG === +# In locale: +# MONGO_URI=mongodb://localhost:27017/mydb +# +# In Docker (usato dal docker-compose): +MONGO_URI=mongodb://root:example@192.168.1.3:27017/myapphttps?authSource=admin +# === UPLOADS === +# Cartella dove Express serve le icone +UPLOAD_DIR=uploads +``` + +avviare + + npm start + +il server parte su 182.168.1.3:3000 ed ho settato nginx come + + my.patachina2.casacam.net + +## User Interface del server + + +andare in + +``` +cd server/frontend +``` + +far partire la UI x es su porta 8282 + + npx http-server . -c-1 -p 8282 + +da qui si possono modificare le apps che vogliamo visualizzare + +## App principale + + +andare in + +``` +cd app +``` +far partire la App x es su porta 8181 + + npx http-server . -c-1 -p 8181 + diff --git a/app/README.md b/app/README.md new file mode 100644 index 0000000..edf8a8a --- /dev/null +++ b/app/README.md @@ -0,0 +1,2 @@ + +npx http-server . diff --git a/app/app.js b/app/app.js new file mode 100644 index 0000000..a479a48 --- /dev/null +++ b/app/app.js @@ -0,0 +1,1006 @@ +// ============================================================================ +// LAUNCHER — VERSIONE COMPLETA E OTTIMIZZATA (A) +// BLOCCO 1/6 — Variabili globali + Storage + Config + Setup Page +// ============================================================================ + +// --------------------------------------------------------------------------- +// VARIABILI GLOBALI +// --------------------------------------------------------------------------- +let URI; +let USER; +let PASSW; + +let appsData = []; // Lista completa delle app (nome, icona, url) +let appsOrder = []; // Ordine delle icone nella griglia +let editMode = false; // Modalità wiggle stile iOS + +// Zoom +let zoomLevel; +let zoomMax; +let initialPinchDistance = null; +let lastTapTime = 0; +let zoomAnimFrame = null; + +// Long‑press / drag +let longPressTimer = null; +let longPressTarget = null; +let contextMenuTargetId = null; + +let draggingIcon = null; +let draggingId = null; +let dragOffsetX = 0; +let dragOffsetY = 0; +let dragStartX = 0; +let dragStartY = 0; +let placeholderEl = null; + +// PATCH MINIMA: flag per evitare che al primo long‑press parta anche il menu +let justEnteredEditMode = false; + +// --------------------------------------------------------------------------- +// CRITTOGRAFIA E STORAGE +// --------------------------------------------------------------------------- +const SECRET_KEY = "chiave-super-segreta-123"; + +// Salva configurazione (URL, user, pass) +function saveConfig(url, user, password) { + const data = { url, user, password }; + URI = url; + USER = user; + PASSW = password; + + const encrypted = CryptoJS.AES.encrypt( + JSON.stringify(data), + SECRET_KEY + ).toString(); + + localStorage.setItem("launcherConfig", encrypted); +} + +// Carica configurazione +function loadConfig() { + const encrypted = localStorage.getItem("launcherConfig"); + if (!encrypted) return null; + + try { + const bytes = CryptoJS.AES.decrypt(encrypted, SECRET_KEY); + const obj = JSON.parse(bytes.toString(CryptoJS.enc.Utf8)); + + URI = obj.url; + USER = obj.user; + PASSW = obj.password; + + return obj; + } catch { + return null; + } +} + +// Salva apps scaricate dal server +function saveApps(jsonApps) { + const encrypted = CryptoJS.AES.encrypt( + JSON.stringify(jsonApps), + SECRET_KEY + ).toString(); + + localStorage.setItem("jsonApps", encrypted); +} + +// Carica apps salvate in locale +function loadApps() { + const encrypted = localStorage.getItem("jsonApps"); + if (!encrypted) return null; + + try { + const bytes = CryptoJS.AES.decrypt(encrypted, SECRET_KEY); + return JSON.parse(bytes.toString(CryptoJS.enc.Utf8)); + } catch { + return null; + } +} + +// --------------------------------------------------------------------------- +// 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; + + // 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"; + } + + document.getElementById("setup-page").classList.remove("hidden"); +} + +function hideSetupPage() { + document.getElementById("setup-page").classList.add("hidden"); +} + +// 6 tap per aprire la setup page +let tapCount = 0; +let tapTimer = null; + +document.addEventListener("click", () => { + tapCount++; + + if (tapTimer) clearTimeout(tapTimer); + + tapTimer = setTimeout(() => { + tapCount = 0; + }, 600); + + if (tapCount >= 6) { + tapCount = 0; + showSetupPage(); + } +}); + +// ============================================================================ +// LAUNCHER — VERSIONE COMPLETA E OTTIMIZZATA (A) +// BLOCCO 2/6 — API login, getLinks, ordine apps, render, startLauncher +// ============================================================================ + +// --------------------------------------------------------------------------- +// LOGIN API +// --------------------------------------------------------------------------- +async function login(email, password) { + try { + const res = await fetch(`${URI}/auth/login`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ email, password }) + }); + + if (!res.ok) { + const text = await res.text(); + throw new Error(`HTTP ${res.status}: ${text}`); + } + + const data = await res.json(); + return data.token; + + } catch (err) { + alert(err); + return null; + } +} + +// --------------------------------------------------------------------------- +// GET LINKS (scarica apps dal server + salva + aggiorna ordine) +// --------------------------------------------------------------------------- +async function getLinks() { + try { + const token = await login(USER, PASSW); + if (!token) throw new Error("User o Password errati"); + + const res = await fetch(`${URI}/links`, { + headers: { + "Authorization": `Bearer ${token}`, + "Accept": "application/json" + } + }); + + if (!res.ok) throw new Error("Server errato o non risponde"); + + const json = await res.json(); + + // Normalizza apps +/* appsData = json.map((a, i) => ({ + id: a.id || `app-${i}`, + name: a.name, + url: a.url, + icon: `${URI}${a.icon}` + })); +*/ +appsData = json.map((a, i) => { + let icon = null; + + if (a.icon && a.icon.data && a.icon.mime) { + const base64 = btoa( + String.fromCharCode(...a.icon.data.data) + ); + icon = `data:${a.icon.mime};base64,${base64}`; + } + + return { + id: a.id || `app-${i}`, + name: a.name, + url: a.url, + icon + }; +}); + + // Salva in locale + saveApps(appsData); + + // Aggiorna ordine + loadAppOrder(); + + // Render + renderApps(); + + return true; + + } catch (err) { + console.error(err); + return null; + } +} + +// --------------------------------------------------------------------------- +// CARICAMENTO ORDINE APPS +// --------------------------------------------------------------------------- +function loadAppOrder() { + const stored = localStorage.getItem("appsOrder"); + + if (stored) { + const parsed = JSON.parse(stored); + + // Mantieni solo ID validi + appsOrder = parsed.filter(id => appsData.some(a => a.id === id)); + + // Aggiungi eventuali nuove app non presenti nell'ordine salvato + appsData.forEach(a => { + if (!appsOrder.includes(a.id)) appsOrder.push(a.id); + }); + + } else { + // Primo avvio → ordine naturale + appsOrder = appsData.map(a => a.id); + } +} + +function saveOrder() { + localStorage.setItem("appsOrder", JSON.stringify(appsOrder)); +} + +// --------------------------------------------------------------------------- +// RENDER DELLA GRIGLIA +// --------------------------------------------------------------------------- +function renderApps() { + const folderEl = document.getElementById("folder"); + folderEl.innerHTML = ""; + + appsOrder.forEach(id => { + const app = appsData.find(a => a.id === id); + if (!app) return; + + const div = document.createElement("div"); + div.className = "app-icon"; + div.dataset.id = app.id; + + div.innerHTML = ` + ${app.name} + ${app.name} + `; + + div.addEventListener("click", () => { + if (!editMode) window.open(app.url, "_blank", "noopener"); + }); + + folderEl.appendChild(div); + }); +} + +// --------------------------------------------------------------------------- +// START LAUNCHER (carica locale → render → init UI) +// --------------------------------------------------------------------------- +async function startLauncher() { + + // 1️⃣ Carica apps salvate in locale + const saved = loadApps(); + if (saved) { + appsData = saved; + console.log("Apps caricate da localStorage:", appsData); + } + + // 2️⃣ Carica ordine + loadAppOrder(); + + // 3️⃣ Render immediato (istantaneo) + renderApps(); + + // ❌ Nessun aggiornamento automatico dal server + // getLinks(); + + // 4️⃣ Inizializza UI (zoom, drag, wiggle, menu…) + initZoomHandlers(); + initLongPressHandlers(); + initDragHandlers(); + initContextMenuActions(); + initGlobalCloseHandlers(); +} + +// ============================================================================ +// LAUNCHER — VERSIONE COMPLETA E OTTIMIZZATA (A) +// BLOCCO 3/6 — Zoom stile iPhone (pinch, elasticità, wheel) +// ============================================================================ + +// --------------------------------------------------------------------------- +// Calcolo dinamico dello zoom massimo +// --------------------------------------------------------------------------- +function computeDynamicMaxZoom() { + return Math.min(window.innerWidth / 85, 4.0); +} + +// --------------------------------------------------------------------------- +// Carica lo zoom salvato in locale +// --------------------------------------------------------------------------- +function loadInitialZoom() { + const v = parseFloat(localStorage.getItem("zoomLevel")); + if (!isFinite(v) || v <= 0) return 1; + return Math.min(Math.max(v, 0.5), computeDynamicMaxZoom()); +} + +// --------------------------------------------------------------------------- +// Applica lo zoom (aggiorna CSS + salva in locale) +// --------------------------------------------------------------------------- +function applyZoom(z) { + zoomLevel = (!isFinite(z) || z <= 0) ? 1 : z; + document.documentElement.style.setProperty("--zoom", zoomLevel); + localStorage.setItem("zoomLevel", String(zoomLevel)); +} + +// --------------------------------------------------------------------------- +// Distanza tra due dita (pinch) +// --------------------------------------------------------------------------- +function getPinchDistance(touches) { + const [a, b] = touches; + return Math.hypot(a.clientX - b.clientX, a.clientY - b.clientY); +} + +// --------------------------------------------------------------------------- +// Elasticità ai limiti (effetto iOS) +// --------------------------------------------------------------------------- +function elasticEase(x) { + return Math.sin(x * Math.PI * 0.5) * 1.05; +} + +// --------------------------------------------------------------------------- +// Inizializzazione completa dello zoom +// --------------------------------------------------------------------------- +function initZoomHandlers() { + zoomMax = computeDynamicMaxZoom(); + zoomLevel = loadInitialZoom(); + applyZoom(zoomLevel); + + // --------------------------------------------------------- + // PINCH SU MOBILE + // --------------------------------------------------------- + + // Previeni scroll durante pinch + document.addEventListener("touchmove", e => { + if (e.touches.length === 2) e.preventDefault(); + }, { passive: false }); + + // Inizio pinch (NO double tap zoom) + document.addEventListener("touchstart", e => { + + // Inizio pinch + if (e.touches.length === 2) { + initialPinchDistance = getPinchDistance(e.touches); + if (zoomAnimFrame) cancelAnimationFrame(zoomAnimFrame); + } + + // Nessuna azione sul doppio tap + lastTapTime = Date.now(); + }); + + // Pinch in corso + document.addEventListener("touchmove", e => { + if (e.touches.length === 2 && initialPinchDistance) { + const newDist = getPinchDistance(e.touches); + const scale = newDist / initialPinchDistance; + + let newZoom = zoomLevel * scale; + zoomMax = computeDynamicMaxZoom(); + + // Elasticità ai limiti + if (newZoom > zoomMax) newZoom = zoomMax + (newZoom - zoomMax) * 0.25; + if (newZoom < 0.5) newZoom = 0.5 - (0.5 - newZoom) * 0.25; + + applyZoom(newZoom); + initialPinchDistance = newDist; + e.preventDefault(); + } + }, { passive: false }); + + // Fine pinch → animazione elastica verso limite + document.addEventListener("touchend", e => { + if (e.touches.length < 2 && initialPinchDistance) { + initialPinchDistance = null; + + zoomMax = computeDynamicMaxZoom(); + const target = Math.min(Math.max(zoomLevel, 0.5), zoomMax); + const start = zoomLevel; + const duration = 250; + const startTime = performance.now(); + + function animate(t) { + const p = Math.min((t - startTime) / duration, 1); + const eased = start + (target - start) * elasticEase(p); + applyZoom(eased); + if (p < 1) zoomAnimFrame = requestAnimationFrame(animate); + } + + zoomAnimFrame = requestAnimationFrame(animate); + } + }); + + // --------------------------------------------------------- + // ZOOM CON WHEEL SU DESKTOP + // --------------------------------------------------------- + document.addEventListener("wheel", e => { + e.preventDefault(); + + zoomMax = computeDynamicMaxZoom(); + const direction = e.deltaY < 0 ? 1 : -1; + const factor = 1 + direction * 0.1; + + let newZoom = zoomLevel * factor; + + // Elasticità + if (newZoom > zoomMax) newZoom = zoomMax + (newZoom - zoomMax) * 0.25; + if (newZoom < 0.5) newZoom = 0.5 - (0.5 - newZoom) * 0.25; + + applyZoom(newZoom); + }, { passive: false }); +} + +// ============================================================================ +// LAUNCHER — VERSIONE COMPLETA E OTTIMIZZATA (A) +// BLOCCO 4/6 — Long‑press, Edit Mode, Context Menu, Global Close +// ============================================================================ + +// --------------------------------------------------------------------------- +// EDIT MODE (wiggle stile iOS) +// --------------------------------------------------------------------------- +function enterEditMode() { + editMode = true; + document.body.classList.add("edit-mode"); +} + +function exitEditMode() { + editMode = false; + document.body.classList.remove("edit-mode"); + hideContextMenu(); +} + +// --------------------------------------------------------------------------- +// MENU CONTESTUALE +// --------------------------------------------------------------------------- +function showContextMenuFor(id, x, y) { + contextMenuTargetId = id; + const menu = document.getElementById("context-menu"); + menu.style.left = `${x}px`; + menu.style.top = `${y}px`; + menu.classList.remove("hidden"); +} + +function hideContextMenu() { + const menu = document.getElementById("context-menu"); + menu.classList.add("hidden"); + contextMenuTargetId = null; +} + +// --------------------------------------------------------------------------- +// LONG PRESS HANDLERS (TOUCH + MOUSE) +// --------------------------------------------------------------------------- +function initLongPressHandlers() { + + // --------------------------------------------------------- + // TOUCH LONG PRESS + // --------------------------------------------------------- + document.addEventListener("touchstart", e => { + if (e.touches.length !== 1) return; + + const touch = e.touches[0]; + const icon = touch.target.closest(".app-icon"); + + if (icon) { + longPressTarget = icon; + + longPressTimer = setTimeout(() => { + + // Primo long‑press → entra in edit mode + if (!editMode) { + enterEditMode(); + justEnteredEditMode = true; // PATCH: blocca il menu su questo primo long-press + if (navigator.vibrate) navigator.vibrate(10); + return; + } + + // Se siamo appena entrati in edit mode con questo long-press → NON aprire menu + if (justEnteredEditMode) { + justEnteredEditMode = false; + return; + } + + // Se già in edit mode da prima → apri menu contestuale + const r = icon.getBoundingClientRect(); + showContextMenuFor( + icon.dataset.id, + r.left + r.width / 2, + r.top + r.height + ); + if (navigator.vibrate) navigator.vibrate(10); + + }, 350); + + return; + } + + // Long press fuori dalle icone → esci da edit mode + longPressTimer = setTimeout(() => { + if (editMode) exitEditMode(); + }, 350); + + }, { passive: true }); + + // Cancella long‑press se l’utente si muove troppo + document.addEventListener("touchmove", e => { + if (!longPressTimer) return; + + const touch = e.touches[0]; + const r = longPressTarget?.getBoundingClientRect(); + + const dx = touch.clientX - (r?.left ?? touch.clientX); + const dy = touch.clientY - (r?.top ?? touch.clientY); + + if (Math.hypot(dx, dy) > 15) { + clearTimeout(longPressTimer); + longPressTimer = null; + longPressTarget = null; + } + }, { passive: true }); + + // Fine touch → cancella long‑press + document.addEventListener("touchend", () => { + if (longPressTimer) { + clearTimeout(longPressTimer); + longPressTimer = null; + longPressTarget = null; + } + // PATCH: reset del flag + justEnteredEditMode = false; + }, { passive: true }); + + // --------------------------------------------------------- + // MOUSE LONG PRESS + // --------------------------------------------------------- + document.addEventListener("mousedown", e => { + if (e.button !== 0) return; + + const icon = e.target.closest(".app-icon"); + longPressTarget = icon ?? null; + + longPressTimer = setTimeout(() => { + + if (!editMode) { + enterEditMode(); + return; + } + + if (icon) { + const r = icon.getBoundingClientRect(); + showContextMenuFor( + icon.dataset.id, + r.left + r.width / 2, + r.top + r.height + ); + } + + }, 350); + }); + + // Cancella long‑press se il mouse si muove troppo + document.addEventListener("mousemove", e => { + if (!longPressTimer) return; + + if (longPressTarget) { + const r = longPressTarget.getBoundingClientRect(); + const dx = e.clientX - (r.left + r.width / 2); + const dy = e.clientY - (r.top + r.height / 2); + + if (Math.hypot(dx, dy) > 15) { + clearTimeout(longPressTimer); + longPressTimer = null; + longPressTarget = null; + } + } + }); + + // Mouse up → cancella long‑press + document.addEventListener("mouseup", () => { + if (longPressTimer) { + clearTimeout(longPressTimer); + longPressTimer = null; + longPressTarget = null; + } + }); +} + +// --------------------------------------------------------------------------- +// CHIUSURA MENU E USCITA DA EDIT MODE +// --------------------------------------------------------------------------- +function initGlobalCloseHandlers() { + document.addEventListener("pointerdown", e => { + const isIcon = e.target.closest(".app-icon"); + const isMenu = e.target.closest("#context-menu"); + + // 1️⃣ Clic fuori dal menu → chiudi menu + if (!isMenu && !isIcon && !document.getElementById("context-menu").classList.contains("hidden")) { + hideContextMenu(); + } + + // 2️⃣ Clic fuori dalle icone → esci da edit mode + if (!isIcon && editMode) { + exitEditMode(); + } + }); +} + +// ============================================================================ +// LAUNCHER — VERSIONE COMPLETA E OTTIMIZZATA (A) +// BLOCCO 5/6 — Drag & Drop stile iPhone (FIXED) +// ============================================================================ + +// ============================================================================ +// LAUNCHER — VERSIONE COMPLETA E OTTIMIZZATA (A) +// BLOCCO 5/6 — Drag & Drop stile iPhone (FIXED) +// ============================================================================ + +// --------------------------------------------------------------------------- +// Utility per ottenere posizione del puntatore (touch + mouse) +// --------------------------------------------------------------------------- +function getPointerPosition(e) { + if (e.touches && e.touches.length > 0) { + return { + pageX: e.touches[0].pageX, + pageY: e.touches[0].pageY, + clientX: e.touches[0].clientX, + clientY: e.touches[0].clientY + }; + } + return { + pageX: e.pageX, + pageY: e.pageY, + clientX: e.clientX, + clientY: e.clientY + }; +} + +// --------------------------------------------------------------------------- +// Inizio drag: icona flottante + placeholder nel layout +// --------------------------------------------------------------------------- +function startDrag(icon, pos) { + const folderEl = document.getElementById("folder"); + + draggingId = icon.dataset.id; + + const r = icon.getBoundingClientRect(); + dragOffsetX = pos.pageX - r.left; + dragOffsetY = pos.pageY - r.top; + + draggingIcon = icon; + draggingIcon.classList.add("dragging"); + draggingIcon.style.position = "fixed"; + draggingIcon.style.left = `${r.left}px`; + draggingIcon.style.top = `${r.top}px`; + draggingIcon.style.width = `${r.width}px`; + draggingIcon.style.height = `${r.height}px`; + draggingIcon.style.zIndex = "1000"; + draggingIcon.style.pointerEvents = "none"; + draggingIcon.style.transform = "translate3d(0,0,0)"; + + // Placeholder nel layout + placeholderEl = document.createElement("div"); + placeholderEl.className = "app-icon placeholder"; + placeholderEl.style.visibility = "hidden"; + + folderEl.insertBefore(placeholderEl, icon); + + hideContextMenu(); +} + +// --------------------------------------------------------------------------- +// Aggiorna posizione icona trascinata + posizione placeholder +// --------------------------------------------------------------------------- +function updateDragPosition(pos) { + if (!draggingIcon || !placeholderEl) return; + + const x = pos.pageX - dragOffsetX; + const y = pos.pageY - dragOffsetY; + + draggingIcon.style.left = `${x}px`; + draggingIcon.style.top = `${y}px`; + + const centerX = pos.clientX; + const centerY = pos.clientY; + + const elem = document.elementFromPoint(centerX, centerY); + const targetIcon = elem && elem.closest(".app-icon:not(.dragging)"); + if (!targetIcon || targetIcon === placeholderEl) return; + + 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: aggiorna appsOrder in base alla posizione del placeholder +// --------------------------------------------------------------------------- +function endDrag() { + if (!draggingIcon || !placeholderEl) { + draggingIcon = null; + placeholderEl = null; + dragStartX = 0; + dragStartY = 0; + return; + } + + const folderEl = document.getElementById("folder"); + + const children = Array.from(folderEl.children); + const finalIndex = children.indexOf(placeholderEl); + + 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 = ""; + + if (finalIndex !== -1) { + const currentIndex = appsOrder.indexOf(draggingId); + if (currentIndex !== -1 && currentIndex !== finalIndex) { + appsOrder.splice(currentIndex, 1); + appsOrder.splice(finalIndex, 0, draggingId); + saveOrder(); + } + } + + if (placeholderEl && placeholderEl.parentNode) { + placeholderEl.parentNode.removeChild(placeholderEl); + } + + draggingIcon = null; + placeholderEl = null; + dragStartX = 0; + dragStartY = 0; + + renderApps(); +} + +// --------------------------------------------------------------------------- +// Inizializzazione Drag & Drop (touch + mouse) +// --------------------------------------------------------------------------- +function initDragHandlers() { + + // TOUCH DRAG + document.addEventListener("touchstart", e => { + if (!editMode) return; + if (e.touches.length !== 1) return; + if (contextMenuTargetId) return; + + const pos = getPointerPosition(e); + const icon = e.touches[0].target.closest(".app-icon"); + if (!icon) return; + + dragStartX = pos.clientX; + dragStartY = pos.clientY; + draggingIcon = null; + draggingId = null; + }, { passive: true }); + + document.addEventListener("touchmove", e => { + if (!editMode) return; + if (e.touches.length !== 1) return; + + const pos = getPointerPosition(e); + + if (!draggingIcon) { + const dx = pos.clientX - dragStartX; + const dy = pos.clientY - dragStartY; + if (Math.hypot(dx, dy) > 10) { + const icon = e.touches[0].target.closest(".app-icon"); + if (icon) { + if (longPressTimer) { + clearTimeout(longPressTimer); + longPressTimer = null; + longPressTarget = null; + } + startDrag(icon, pos); + } + } + } else { + updateDragPosition(pos); + e.preventDefault(); + } + }, { passive: false }); + + document.addEventListener("touchend", e => { + if (!editMode) return; + if (!draggingIcon) { + dragStartX = 0; + dragStartY = 0; + return; + } + if (!e.touches || e.touches.length === 0) { + endDrag(); + } + }, { passive: true }); + + // MOUSE DRAG + document.addEventListener("mousedown", e => { + if (!editMode) return; + if (e.button !== 0) return; + if (contextMenuTargetId) return; + + const icon = e.target.closest(".app-icon"); + if (!icon) return; + + const pos = getPointerPosition(e); + dragStartX = pos.clientX; + dragStartY = pos.clientY; + 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; + if (!draggingIcon) { + dragStartX = 0; + dragStartY = 0; + return; + } + endDrag(); + }); +} + +// ============================================================================ +// BLOCCO 6/6 — Context Menu Actions + Config Save + Init Globale +// ============================================================================ + +// --------------------------------------------------------------------------- +// MENU CONTESTUALE — AZIONI (rename, change icon, remove) +// --------------------------------------------------------------------------- +function initContextMenuActions() { + const menu = document.getElementById("context-menu"); + + menu.addEventListener("click", e => { + const btn = e.target.closest("button"); + if (!btn || !contextMenuTargetId) return; + + const action = btn.dataset.action; + const app = appsData.find(a => a.id === contextMenuTargetId); + if (!app) return; + + // RINOMINA + if (action === "rename") { + const nuovoNome = prompt("Nuovo nome app:", app.name); + if (nuovoNome && nuovoNome.trim()) { + app.name = nuovoNome.trim(); + renderApps(); + saveOrder(); + } + } + + // CAMBIA ICONA + if (action === "change-icon") { + const nuovaIcona = prompt("URL nuova icona:", app.icon); + if (nuovaIcona && nuovaIcona.trim()) { + app.icon = nuovaIcona.trim(); + renderApps(); + saveOrder(); + } + } + + // RIMUOVI + if (action === "remove") { + if (confirm("Rimuovere questa app dalla griglia?")) { + appsOrder = appsOrder.filter(id => id !== app.id); + saveOrder(); + renderApps(); + } + } + + hideContextMenu(); + }); +} + +// --------------------------------------------------------------------------- +// AGGIORNA ORA — aggiorna apps dal server senza cambiare config +// --------------------------------------------------------------------------- +document.getElementById("cfg-refresh").addEventListener("click", async () => { + + const cfg = loadConfig(); + if (!cfg) { + alert("Config mancante. Inserisci URL, user e password."); + return; + } + + const ok = await getLinks(); + + if (ok) { + hideSetupPage(); + startLauncher(); + } else { + alert("Impossibile aggiornare le app dal server."); + } +}); + +// --------------------------------------------------------------------------- +// SALVATAGGIO CONFIG + RESTART COMPLETO +// --------------------------------------------------------------------------- +document.getElementById("cfg-save").addEventListener("click", async () => { + const url = document.getElementById("cfg-url").value; + const user = document.getElementById("cfg-user").value; + const pass = document.getElementById("cfg-pass").value; + + saveConfig(url, user, pass); + + const ok = await getLinks(); + + if (ok) { + hideSetupPage(); + startLauncher(); + } +}); + +// --------------------------------------------------------------------------- +// INIT GLOBALE — DOMContentLoaded +// --------------------------------------------------------------------------- +document.addEventListener("DOMContentLoaded", () => { + const cfg = loadConfig(); + + if (!cfg) { + showSetupPage(); + } else { + hideSetupPage(); + startLauncher(); + } +}); diff --git a/app/app.js.old b/app/app.js.old new file mode 100644 index 0000000..e118506 --- /dev/null +++ b/app/app.js.old @@ -0,0 +1,983 @@ +// ============================================================================ +// LAUNCHER — VERSIONE COMPLETA E OTTIMIZZATA (A) +// BLOCCO 1/6 — Variabili globali + Storage + Config + Setup Page +// ============================================================================ + +// --------------------------------------------------------------------------- +// VARIABILI GLOBALI +// --------------------------------------------------------------------------- +let URI; +let USER; +let PASSW; + +let appsData = []; // Lista completa delle app (nome, icona, url) +let appsOrder = []; // Ordine delle icone nella griglia +let editMode = false; // Modalità wiggle stile iOS + +// Zoom +let zoomLevel; +let zoomMax; +let initialPinchDistance = null; +let lastTapTime = 0; +let zoomAnimFrame = null; + +// Long‑press / drag +let longPressTimer = null; +let longPressTarget = null; +let contextMenuTargetId = null; + +let draggingIcon = null; +let draggingId = null; +let dragOffsetX = 0; +let dragOffsetY = 0; +let dragStartX = 0; +let dragStartY = 0; +let placeholderEl = null; + +// --------------------------------------------------------------------------- +// CRITTOGRAFIA E STORAGE +// --------------------------------------------------------------------------- +const SECRET_KEY = "chiave-super-segreta-123"; + +// Salva configurazione (URL, user, pass) +function saveConfig(url, user, password) { + const data = { url, user, password }; + URI = url; + USER = user; + PASSW = password; + + const encrypted = CryptoJS.AES.encrypt( + JSON.stringify(data), + SECRET_KEY + ).toString(); + + localStorage.setItem("launcherConfig", encrypted); +} + +// Carica configurazione +function loadConfig() { + const encrypted = localStorage.getItem("launcherConfig"); + if (!encrypted) return null; + + try { + const bytes = CryptoJS.AES.decrypt(encrypted, SECRET_KEY); + const obj = JSON.parse(bytes.toString(CryptoJS.enc.Utf8)); + + URI = obj.url; + USER = obj.user; + PASSW = obj.password; + + return obj; + } catch { + return null; + } +} + +// Salva apps scaricate dal server +function saveApps(jsonApps) { + const encrypted = CryptoJS.AES.encrypt( + JSON.stringify(jsonApps), + SECRET_KEY + ).toString(); + + localStorage.setItem("jsonApps", encrypted); +} + +// Carica apps salvate in locale +function loadApps() { + const encrypted = localStorage.getItem("jsonApps"); + if (!encrypted) return null; + + try { + const bytes = CryptoJS.AES.decrypt(encrypted, SECRET_KEY); + return JSON.parse(bytes.toString(CryptoJS.enc.Utf8)); + } catch { + return null; + } +} + +// --------------------------------------------------------------------------- +// 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; + + // 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"; + } + + document.getElementById("setup-page").classList.remove("hidden"); +} + +function hideSetupPage() { + document.getElementById("setup-page").classList.add("hidden"); +} + +// 6 tap per aprire la setup page +let tapCount = 0; +let tapTimer = null; + +document.addEventListener("click", () => { + tapCount++; + + if (tapTimer) clearTimeout(tapTimer); + + tapTimer = setTimeout(() => { + tapCount = 0; + }, 600); + + if (tapCount >= 6) { + tapCount = 0; + showSetupPage(); + } +}); + +// ============================================================================ +// LAUNCHER — VERSIONE COMPLETA E OTTIMIZZATA (A) +// BLOCCO 2/6 — API login, getLinks, ordine apps, render, startLauncher +// ============================================================================ + +// --------------------------------------------------------------------------- +// LOGIN API +// --------------------------------------------------------------------------- +async function login(email, password) { + try { + const res = await fetch(`${URI}/auth/login`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ email, password }) + }); + + if (!res.ok) { + const text = await res.text(); + throw new Error(`HTTP ${res.status}: ${text}`); + } + + const data = await res.json(); + return data.token; + + } catch (err) { + alert(err); + return null; + } +} + +// --------------------------------------------------------------------------- +// GET LINKS (scarica apps dal server + salva + aggiorna ordine) +// --------------------------------------------------------------------------- +async function getLinks() { + try { + const token = await login(USER, PASSW); + if (!token) throw new Error("User o Password errati"); + + const res = await fetch(`${URI}/links`, { + headers: { + "Authorization": `Bearer ${token}`, + "Accept": "application/json" + } + }); + + if (!res.ok) throw new Error("Server errato o non risponde"); + + const json = await res.json(); + + // Normalizza apps + appsData = json.map((a, i) => ({ + id: a.id || `app-${i}`, + name: a.name, + url: a.url, + icon: `${URI}${a.icon}` + })); + + // Salva in locale + saveApps(appsData); + + // Aggiorna ordine + loadAppOrder(); + + // Render + renderApps(); + + return true; + + } catch (err) { + console.error(err); + return null; + } +} + +// --------------------------------------------------------------------------- +// CARICAMENTO ORDINE APPS +// --------------------------------------------------------------------------- +function loadAppOrder() { + const stored = localStorage.getItem("appsOrder"); + + if (stored) { + const parsed = JSON.parse(stored); + + // Mantieni solo ID validi + appsOrder = parsed.filter(id => appsData.some(a => a.id === id)); + + // Aggiungi eventuali nuove app non presenti nell'ordine salvato + appsData.forEach(a => { + if (!appsOrder.includes(a.id)) appsOrder.push(a.id); + }); + + } else { + // Primo avvio → ordine naturale + appsOrder = appsData.map(a => a.id); + } +} + +function saveOrder() { + localStorage.setItem("appsOrder", JSON.stringify(appsOrder)); +} + +// --------------------------------------------------------------------------- +// RENDER DELLA GRIGLIA +// --------------------------------------------------------------------------- +function renderApps() { + const folderEl = document.getElementById("folder"); + folderEl.innerHTML = ""; + + appsOrder.forEach(id => { + const app = appsData.find(a => a.id === id); + if (!app) return; + + const div = document.createElement("div"); + div.className = "app-icon"; + div.dataset.id = app.id; + + div.innerHTML = ` + ${app.name} + ${app.name} + `; + + div.addEventListener("click", () => { + if (!editMode) window.open(app.url, "_blank", "noopener"); + }); + + folderEl.appendChild(div); + }); +} + +// --------------------------------------------------------------------------- +// START LAUNCHER (carica locale → render → init UI) +// --------------------------------------------------------------------------- +async function startLauncher() { + + // 1️⃣ Carica apps salvate in locale + const saved = loadApps(); + if (saved) { + appsData = saved; + console.log("Apps caricate da localStorage:", appsData); + } + + // 2️⃣ Carica ordine + loadAppOrder(); + + // 3️⃣ Render immediato (istantaneo) + renderApps(); + + // ❌ Nessun aggiornamento automatico dal server + // getLinks(); + + // 4️⃣ Inizializza UI (zoom, drag, wiggle, menu…) + initZoomHandlers(); + initLongPressHandlers(); + initDragHandlers(); + initContextMenuActions(); + initGlobalCloseHandlers(); +} + +// ============================================================================ +// LAUNCHER — VERSIONE COMPLETA E OTTIMIZZATA (A) +// BLOCCO 3/6 — Zoom stile iPhone (pinch, elasticità, wheel) +// ============================================================================ + +// --------------------------------------------------------------------------- +// Calcolo dinamico dello zoom massimo +// --------------------------------------------------------------------------- +function computeDynamicMaxZoom() { + return Math.min(window.innerWidth / 85, 4.0); +} + +// --------------------------------------------------------------------------- +// Carica lo zoom salvato in locale +// --------------------------------------------------------------------------- +function loadInitialZoom() { + const v = parseFloat(localStorage.getItem("zoomLevel")); + if (!isFinite(v) || v <= 0) return 1; + return Math.min(Math.max(v, 0.5), computeDynamicMaxZoom()); +} + +// --------------------------------------------------------------------------- +// Applica lo zoom (aggiorna CSS + salva in locale) +// --------------------------------------------------------------------------- +function applyZoom(z) { + zoomLevel = (!isFinite(z) || z <= 0) ? 1 : z; + document.documentElement.style.setProperty("--zoom", zoomLevel); + localStorage.setItem("zoomLevel", String(zoomLevel)); +} + +// --------------------------------------------------------------------------- +// Distanza tra due dita (pinch) +// --------------------------------------------------------------------------- +function getPinchDistance(touches) { + const [a, b] = touches; + return Math.hypot(a.clientX - b.clientX, a.clientY - b.clientY); +} + +// --------------------------------------------------------------------------- +// Elasticità ai limiti (effetto iOS) +// --------------------------------------------------------------------------- +function elasticEase(x) { + return Math.sin(x * Math.PI * 0.5) * 1.05; +} + +// --------------------------------------------------------------------------- +// Inizializzazione completa dello zoom +// --------------------------------------------------------------------------- +function initZoomHandlers() { + zoomMax = computeDynamicMaxZoom(); + zoomLevel = loadInitialZoom(); + applyZoom(zoomLevel); + + // --------------------------------------------------------- + // PINCH SU MOBILE + // --------------------------------------------------------- + + // Previeni scroll durante pinch + document.addEventListener("touchmove", e => { + if (e.touches.length === 2) e.preventDefault(); + }, { passive: false }); + + // Inizio pinch (NO double tap zoom) + document.addEventListener("touchstart", e => { + + // Inizio pinch + if (e.touches.length === 2) { + initialPinchDistance = getPinchDistance(e.touches); + if (zoomAnimFrame) cancelAnimationFrame(zoomAnimFrame); + } + + // Nessuna azione sul doppio tap + lastTapTime = Date.now(); + }); + + // Pinch in corso + document.addEventListener("touchmove", e => { + if (e.touches.length === 2 && initialPinchDistance) { + const newDist = getPinchDistance(e.touches); + const scale = newDist / initialPinchDistance; + + let newZoom = zoomLevel * scale; + zoomMax = computeDynamicMaxZoom(); + + // Elasticità ai limiti + if (newZoom > zoomMax) newZoom = zoomMax + (newZoom - zoomMax) * 0.25; + if (newZoom < 0.5) newZoom = 0.5 - (0.5 - newZoom) * 0.25; + + applyZoom(newZoom); + initialPinchDistance = newDist; + e.preventDefault(); + } + }, { passive: false }); + + // Fine pinch → animazione elastica verso limite + document.addEventListener("touchend", e => { + if (e.touches.length < 2 && initialPinchDistance) { + initialPinchDistance = null; + + zoomMax = computeDynamicMaxZoom(); + const target = Math.min(Math.max(zoomLevel, 0.5), zoomMax); + const start = zoomLevel; + const duration = 250; + const startTime = performance.now(); + + function animate(t) { + const p = Math.min((t - startTime) / duration, 1); + const eased = start + (target - start) * elasticEase(p); + applyZoom(eased); + if (p < 1) zoomAnimFrame = requestAnimationFrame(animate); + } + + zoomAnimFrame = requestAnimationFrame(animate); + } + }); + + // --------------------------------------------------------- + // ZOOM CON WHEEL SU DESKTOP + // --------------------------------------------------------- + document.addEventListener("wheel", e => { + e.preventDefault(); + + zoomMax = computeDynamicMaxZoom(); + const direction = e.deltaY < 0 ? 1 : -1; + const factor = 1 + direction * 0.1; + + let newZoom = zoomLevel * factor; + + // Elasticità + if (newZoom > zoomMax) newZoom = zoomMax + (newZoom - zoomMax) * 0.25; + if (newZoom < 0.5) newZoom = 0.5 - (0.5 - newZoom) * 0.25; + + applyZoom(newZoom); + }, { passive: false }); +} + +// ============================================================================ +// LAUNCHER — VERSIONE COMPLETA E OTTIMIZZATA (A) +// BLOCCO 4/6 — Long‑press, Edit Mode, Context Menu, Global Close +// ============================================================================ + +// --------------------------------------------------------------------------- +// EDIT MODE (wiggle stile iOS) +// --------------------------------------------------------------------------- +function enterEditMode() { + editMode = true; + document.body.classList.add("edit-mode"); +} + +function exitEditMode() { + editMode = false; + document.body.classList.remove("edit-mode"); + hideContextMenu(); +} + +// --------------------------------------------------------------------------- +// MENU CONTESTUALE +// --------------------------------------------------------------------------- +function showContextMenuFor(id, x, y) { + contextMenuTargetId = id; + const menu = document.getElementById("context-menu"); + menu.style.left = `${x}px`; + menu.style.top = `${y}px`; + menu.classList.remove("hidden"); +} + +function hideContextMenu() { + const menu = document.getElementById("context-menu"); + menu.classList.add("hidden"); + contextMenuTargetId = null; +} + +// --------------------------------------------------------------------------- +// LONG PRESS HANDLERS (TOUCH + MOUSE) +// --------------------------------------------------------------------------- +function initLongPressHandlers() { + + // --------------------------------------------------------- + // TOUCH LONG PRESS + // --------------------------------------------------------- + document.addEventListener("touchstart", e => { + if (e.touches.length !== 1) return; + + const touch = e.touches[0]; + const icon = touch.target.closest(".app-icon"); + + if (icon) { + longPressTarget = icon; + + longPressTimer = setTimeout(() => { + + // Primo long‑press → entra in edit mode + if (!editMode) { + enterEditMode(); + if (navigator.vibrate) navigator.vibrate(10); + return; + } + + // Se già in edit mode → apri menu contestuale + const r = icon.getBoundingClientRect(); + showContextMenuFor( + icon.dataset.id, + r.left + r.width / 2, + r.top + r.height + ); + if (navigator.vibrate) navigator.vibrate(10); + + }, 350); + + return; + } + + // Long press fuori dalle icone → esci da edit mode + longPressTimer = setTimeout(() => { + if (editMode) exitEditMode(); + }, 350); + + }, { passive: true }); + + // Cancella long‑press se l’utente si muove troppo + document.addEventListener("touchmove", e => { + if (!longPressTimer) return; + + const touch = e.touches[0]; + const r = longPressTarget?.getBoundingClientRect(); + + const dx = touch.clientX - (r?.left ?? touch.clientX); + const dy = touch.clientY - (r?.top ?? touch.clientY); + + if (Math.hypot(dx, dy) > 15) { + clearTimeout(longPressTimer); + longPressTimer = null; + longPressTarget = null; + } + }, { passive: true }); + + // Fine touch → cancella long‑press + document.addEventListener("touchend", () => { + if (longPressTimer) { + clearTimeout(longPressTimer); + longPressTimer = null; + longPressTarget = null; + } + }, { passive: true }); + + // --------------------------------------------------------- + // MOUSE LONG PRESS + // --------------------------------------------------------- + document.addEventListener("mousedown", e => { + if (e.button !== 0) return; + + const icon = e.target.closest(".app-icon"); + longPressTarget = icon ?? null; + + longPressTimer = setTimeout(() => { + + if (!editMode) { + enterEditMode(); + return; + } + + if (icon) { + const r = icon.getBoundingClientRect(); + showContextMenuFor( + icon.dataset.id, + r.left + r.width / 2, + r.top + r.height + ); + } + + }, 350); + }); + + // Cancella long‑press se il mouse si muove troppo + document.addEventListener("mousemove", e => { + if (!longPressTimer) return; + + if (longPressTarget) { + const r = longPressTarget.getBoundingClientRect(); + const dx = e.clientX - (r.left + r.width / 2); + const dy = e.clientY - (r.top + r.height / 2); + + if (Math.hypot(dx, dy) > 15) { + clearTimeout(longPressTimer); + longPressTimer = null; + longPressTarget = null; + } + } + }); + + // Mouse up → cancella long‑press + document.addEventListener("mouseup", () => { + if (longPressTimer) { + clearTimeout(longPressTimer); + longPressTimer = null; + longPressTarget = null; + } + }); +} + +// --------------------------------------------------------------------------- +// CHIUSURA MENU E USCITA DA EDIT MODE +// --------------------------------------------------------------------------- +function initGlobalCloseHandlers() { + document.addEventListener("pointerdown", e => { + const isIcon = e.target.closest(".app-icon"); + const isMenu = e.target.closest("#context-menu"); + + // 1️⃣ Clic fuori dal menu → chiudi menu + if (!isMenu && !isIcon && !document.getElementById("context-menu").classList.contains("hidden")) { + hideContextMenu(); + } + + // 2️⃣ Clic fuori dalle icone → esci da edit mode + if (!isIcon && editMode) { + exitEditMode(); + } + }); +} + +// ============================================================================ +// LAUNCHER — VERSIONE COMPLETA E OTTIMIZZATA (A) +// BLOCCO 5/6 — Drag & Drop stile iPhone (FIXED) +// ============================================================================ + +// --------------------------------------------------------------------------- +// Utility per ottenere posizione del puntatore (touch + mouse) +// --------------------------------------------------------------------------- +function getPointerPosition(e) { + if (e.touches && e.touches.length > 0) { + return { + pageX: e.touches[0].pageX, + pageY: e.touches[0].pageY, + clientX: e.touches[0].clientX, + clientY: e.touches[0].clientY + }; + } + return { + pageX: e.pageX, + pageY: e.pageY, + clientX: e.clientX, + clientY: e.clientY + }; +} + +// --------------------------------------------------------------------------- +/* Inizio drag: icona flottante + placeholder nel layout */ +// --------------------------------------------------------------------------- +function startDrag(icon, pos) { + const folderEl = document.getElementById("folder"); + + draggingId = icon.dataset.id; + + const r = icon.getBoundingClientRect(); + dragOffsetX = pos.pageX - r.left; + dragOffsetY = pos.pageY - r.top; + + draggingIcon = icon; + draggingIcon.classList.add("dragging"); + draggingIcon.style.position = "fixed"; + draggingIcon.style.left = `${r.left}px`; + draggingIcon.style.top = `${r.top}px`; + draggingIcon.style.width = `${r.width}px`; + draggingIcon.style.height = `${r.height}px`; + draggingIcon.style.zIndex = "1000"; + draggingIcon.style.pointerEvents = "none"; + draggingIcon.style.transform = "translate3d(0,0,0)"; + + // 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 icona trascinata + posizione placeholder +// --------------------------------------------------------------------------- +function updateDragPosition(pos) { + if (!draggingIcon || !placeholderEl) return; + + const x = pos.pageX - dragOffsetX; + const y = pos.pageY - dragOffsetY; + + draggingIcon.style.left = `${x}px`; + draggingIcon.style.top = `${y}px`; + + const centerX = pos.clientX; + const centerY = pos.clientY; + + const elem = document.elementFromPoint(centerX, centerY); + const targetIcon = elem && elem.closest(".app-icon:not(.dragging)"); + if (!targetIcon || targetIcon === placeholderEl) return; + + 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: aggiorna appsOrder in base alla posizione del placeholder +// --------------------------------------------------------------------------- +function endDrag() { + if (!draggingIcon || !placeholderEl) { + draggingIcon = null; + placeholderEl = null; + dragStartX = 0; + dragStartY = 0; + return; + } + + const folderEl = document.getElementById("folder"); + + // Tutti i figli, inclusa la placeholder + const children = Array.from(folderEl.children); + const finalIndex = children.indexOf(placeholderEl); + + // 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 = ""; + + if (finalIndex !== -1) { + const currentIndex = appsOrder.indexOf(draggingId); + if (currentIndex !== -1 && currentIndex !== finalIndex) { + appsOrder.splice(currentIndex, 1); + appsOrder.splice(finalIndex, 0, draggingId); + saveOrder(); + } + } + + if (placeholderEl && placeholderEl.parentNode) { + placeholderEl.parentNode.removeChild(placeholderEl); + } + + draggingIcon = null; + placeholderEl = null; + dragStartX = 0; + dragStartY = 0; + + // Ridisegna in base al nuovo ordine + renderApps(); +} + +// --------------------------------------------------------------------------- +// Inizializzazione Drag & Drop (touch + mouse) +// --------------------------------------------------------------------------- +function initDragHandlers() { + + // TOUCH DRAG + document.addEventListener("touchstart", e => { + if (!editMode) return; + if (e.touches.length !== 1) return; + if (contextMenuTargetId) return; + + const pos = getPointerPosition(e); + const icon = e.touches[0].target.closest(".app-icon"); + if (!icon) return; + + dragStartX = pos.clientX; + dragStartY = pos.clientY; + draggingIcon = null; + draggingId = null; + }, { passive: true }); + + document.addEventListener("touchmove", e => { + if (!editMode) return; + if (e.touches.length !== 1) return; + + const pos = getPointerPosition(e); + + if (!draggingIcon) { + const dx = pos.clientX - dragStartX; + const dy = pos.clientY - dragStartY; + if (Math.hypot(dx, dy) > 10) { + const icon = e.touches[0].target.closest(".app-icon"); + if (icon) { + if (longPressTimer) { + clearTimeout(longPressTimer); + longPressTimer = null; + longPressTarget = null; + } + startDrag(icon, pos); + } + } + } else { + updateDragPosition(pos); + e.preventDefault(); + } + }, { passive: false }); + + document.addEventListener("touchend", e => { + if (!editMode) return; + if (!draggingIcon) { + dragStartX = 0; + dragStartY = 0; + return; + } + if (!e.touches || e.touches.length === 0) { + endDrag(); + } + }, { passive: true }); + + // MOUSE DRAG + document.addEventListener("mousedown", e => { + if (!editMode) return; + if (e.button !== 0) return; + if (contextMenuTargetId) return; + + const icon = e.target.closest(".app-icon"); + if (!icon) return; + + const pos = getPointerPosition(e); + dragStartX = pos.clientX; + dragStartY = pos.clientY; + 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; + if (!draggingIcon) { + dragStartX = 0; + dragStartY = 0; + return; + } + endDrag(); + }); +} + +// ============================================================================ +// LAUNCHER — VERSIONE COMPLETA E +// OTTIMIZZATA (A) BLOCCO 6/6 — Context Menu +// Actions + Config Save + Init Globale +// ============================================================================ + +// --------------------------------------------------------------------------- +// MENU CONTESTUALE — AZIONI (rename, change icon, remove) +// --------------------------------------------------------------------------- +function initContextMenuActions() { + const menu = document.getElementById("context-menu"); + + menu.addEventListener("click", e => { + const btn = e.target.closest("button"); + if (!btn || !contextMenuTargetId) return; + + const action = btn.dataset.action; + const app = appsData.find(a => a.id === contextMenuTargetId); + if (!app) return; + + // --------------------------------------------------------- + // RINOMINA APP + // --------------------------------------------------------- + if (action === "rename") { + const nuovoNome = prompt("Nuovo nome app:", app.name); + if (nuovoNome && nuovoNome.trim()) { + app.name = nuovoNome.trim(); + renderApps(); + saveOrder(); + } + } + + // --------------------------------------------------------- + // CAMBIA ICONA + // --------------------------------------------------------- + if (action === "change-icon") { + const nuovaIcona = prompt("URL nuova icona:", app.icon); + if (nuovaIcona && nuovaIcona.trim()) { + app.icon = nuovaIcona.trim(); + renderApps(); + saveOrder(); + } + } + + // --------------------------------------------------------- + // RIMUOVI APP DALLA GRIGLIA + // --------------------------------------------------------- + if (action === "remove") { + if (confirm("Rimuovere questa app dalla griglia?")) { + appsOrder = appsOrder.filter(id => id !== app.id); + saveOrder(); + renderApps(); + } + } + + hideContextMenu(); + }); +} + +// --------------------------------------------------------------------------- +// AGGIORNA ORA — aggiorna apps dal server senza cambiare config +// --------------------------------------------------------------------------- +document.getElementById("cfg-refresh").addEventListener("click", async () => { + + const cfg = loadConfig(); + if (!cfg) { + alert("Config mancante. Inserisci URL, user e password."); + return; + } + + const ok = await getLinks(); + + if (ok) { + hideSetupPage(); + startLauncher(); // Torna subito alla schermata principale + } else { + alert("Impossibile aggiornare le app dal server."); + } +}); + +// --------------------------------------------------------------------------- +// SALVATAGGIO CONFIG + RESTART COMPLETO +// --------------------------------------------------------------------------- +document.getElementById("cfg-save").addEventListener("click", async () => { + const url = document.getElementById("cfg-url").value; + const user = document.getElementById("cfg-user").value; + const pass = document.getElementById("cfg-pass").value; + + saveConfig(url, user, pass); + + const ok = await getLinks(); + + if (ok) { + hideSetupPage(); + startLauncher(); + } +}); + +// --------------------------------------------------------------------------- +// INIT GLOBALE — DOMContentLoaded +// --------------------------------------------------------------------------- +document.addEventListener("DOMContentLoaded", () => { + const cfg = loadConfig(); + + if (!cfg) { + showSetupPage(); + } else { + hideSetupPage(); + startLauncher(); + } +}); diff --git a/app/index.html b/app/index.html new file mode 100644 index 0000000..ba09ee9 --- /dev/null +++ b/app/index.html @@ -0,0 +1,48 @@ + + + + + Launcher + + + + + + + + + + +
+ + + + + + + + + + diff --git a/app/start.sh b/app/start.sh new file mode 100755 index 0000000..6c32c02 --- /dev/null +++ b/app/start.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +# Avvia un server HTTP sulla porta 11002 senza cache +npx http-server . -c-1 -p 11002 diff --git a/app/style.css b/app/style.css new file mode 100644 index 0000000..1b7b449 --- /dev/null +++ b/app/style.css @@ -0,0 +1,259 @@ +/* ============================================================ + BASE PAGE + ============================================================ */ + +html, body { + margin: 0; + padding: 0; + overflow-x: hidden; /* impedisce pan orizzontale */ + max-width: 100%; + touch-action: pan-y; /* solo scroll verticale */ + background: #ffffff; + /*background: radial-gradient(circle at top, #f8f9ff 0%, #e6e8ef 60%, #dcdfe6 100%);*/ + font-family: system-ui, -apple-system, BlinkMacSystemFont, sans-serif; + color: #1a1a1a; + min-height: 100vh; /* evita scroll inutile se poche icone */ +} + +/* Impedisce selezione testo e highlight blu Android */ +* { + -webkit-user-select: none; + user-select: none; + -webkit-tap-highlight-color: transparent; +} + +/* Variabile di zoom globale */ +:root { + --zoom: 1; +} + +/* ============================================================ + GRIGLIA ICONE + ============================================================ */ + +.folder { + display: grid; + grid-template-columns: repeat( + auto-fill, + minmax(calc(85px * var(--zoom)), 1fr) + ); + gap: calc(16px * var(--zoom)); + padding-top: 24px; + padding-left: 24px; + padding-right: 24px; + padding-bottom: 0; + justify-items: start; /* più coerente con iOS */ + width: 100%; + max-width: 100%; + box-sizing: border-box; + + transition: grid-template-columns 0.15s ease-out, + gap 0.15s ease-out; +} + +/* Contenitore icona — versione glass */ +.app-icon { + text-align: center; + cursor: pointer; + user-select: none; + touch-action: none; + transition: transform 0.18s ease, filter 0.18s ease; + + /* GLASS */ + background: rgba(255, 255, 255, 0.12); + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); + + border-radius: calc(20px * var(--zoom)); + padding: calc(6px * var(--zoom)); + box-sizing: border-box; + overflow: hidden; +} + +/* Icona PNG */ +.app-icon img { + width: calc(78px * var(--zoom)); + height: calc(78px * var(--zoom)); + border-radius: calc(16px * var(--zoom)); + background: transparent; + pointer-events: none; + + box-shadow: + 0 4px 10px rgba(0, 0, 0, 0.12), + 0 8px 24px rgba(0, 0, 0, 0.08); + + display: block; +} + +/* Etichetta */ +.app-icon span { + display: block; + margin-top: calc(6px * var(--zoom)); + font-size: calc(11px * var(--zoom)); + color: #3a3a3a; + font-weight: 500; + letter-spacing: -0.2px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + + transition: font-size 0.18s ease-out, + margin-top 0.18s ease-out; +} + + +/* ============================================================ + WIGGLE MODE + ============================================================ */ + +@keyframes wiggle { + 0% { transform: rotate(-2deg) scale(1.02); } + 50% { transform: rotate( 2deg) scale(0.98); } + 100% { transform: rotate(-2deg) scale(1.02); } +} + +.edit-mode .app-icon:not(.dragging) img { + animation: wiggle 0.25s ease-in-out infinite; +} + +/* Icona trascinata */ +.app-icon.dragging { + opacity: 0.9; + z-index: 1000; +} + +/* Placeholder invisibile */ +.app-icon.placeholder { + opacity: 0; + visibility: hidden; +} + +/* ============================================================ + MENU CONTESTUALE — ANDROID MATERIAL + RESPONSIVE ALLO ZOOM + ============================================================ */ + +#context-menu { + position: fixed; + background: #ffffff; + border-radius: calc(14px * var(--zoom)); + min-width: calc(180px * var(--zoom)); + padding: calc(8px * var(--zoom)) 0; + z-index: 2000; + + /* Ombra Material */ + box-shadow: + 0 calc(6px * var(--zoom)) calc(20px * var(--zoom)) rgba(0,0,0,0.18), + 0 calc(2px * var(--zoom)) calc(6px * var(--zoom)) rgba(0,0,0,0.12); + + /* Animazione apertura */ + opacity: 0; + transform: scale(0.85); + transform-origin: top center; + transition: opacity 120ms ease, transform 120ms ease; +} + +#context-menu:not(.hidden) { + opacity: 1; + transform: scale(1); +} + +#context-menu.hidden { + display: block; + opacity: 0; + pointer-events: none; +} + +/* Pulsanti del menù */ +#context-menu button { + all: unset; + width: 100%; + padding: calc(14px * var(--zoom)) calc(18px * var(--zoom)); + font-size: calc(15px * var(--zoom)); + color: #222; + display: flex; + align-items: center; + gap: calc(12px * var(--zoom)); + cursor: pointer; + position: relative; + overflow: hidden; +} + +/* Ripple effect */ +#context-menu button::after { + content: ""; + position: absolute; + inset: 0; + background: rgba(0,0,0,0.08); + opacity: 0; + transition: opacity 150ms; +} + +#context-menu button:active::after { + opacity: 1; +} + +/* Separatore tra voci */ +#context-menu button + button { + border-top: 1px solid rgba(0,0,0,0.08); +} + +/* Voce "Rimuovi" in rosso */ +#context-menu button:last-child { + color: #d11a2a; +} + + + +/* Permette drag sia mouse che touch */ +.app-icon { + touch-action: none; +} + +/* Evita che l'immagine intercetti eventi */ +.app-icon img { + pointer-events: none; +} + +/* Allineamento stile iOS, evita offset su PC */ +.folder { + justify-items: start; +} + +/* ============================================================ + PAGINA INIZIALE + ============================================================ */ + +#setup-page { + position: fixed; + inset: 0; + background: #f5f5f7; + padding: 40px; + display: flex; + flex-direction: column; + gap: 16px; + z-index: 9999; +} + +#setup-page.hidden { + display: none; +} + +#setup-page input { + padding: 12px; + font-size: 16px; + border-radius: 8px; + border: 1px solid #ccc; +} + +#setup-page button { + padding: 14px; + font-size: 16px; + border-radius: 8px; + background: #007aff; + color: white; + border: none; +} + +#cfg-refresh { + display: none; +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..22f33fa --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,33 @@ +version: "3.9" + +services: + backend: + build: ./server/backend + container_name: backend + restart: unless-stopped + environment: + - MONGO_URI=mongodb://root:example@192.168.1.3:27017/myapphttps?authSource=admin + - JWT_SECRET=master66 + - PORT=11001 + ports: + - "11001:11001" + volumes: + - /home/nvme/dockerdata/myapps/icons:/app/uploads + + frontend_server: + image: nginx:alpine + container_name: frontend_server + restart: unless-stopped + ports: + - "11003:80" + volumes: + - ./server/frontend:/usr/share/nginx/html:ro + + app_static: + image: nginx:alpine + container_name: app_static + restart: unless-stopped + ports: + - "11002:80" + volumes: + - ./app:/usr/share/nginx/html:ro diff --git a/docker_run.sh b/docker_run.sh new file mode 100755 index 0000000..9e59cef --- /dev/null +++ b/docker_run.sh @@ -0,0 +1,2 @@ +#!/bin/bash +sudo docker compose up -d diff --git a/server/README.md b/server/README.md new file mode 100644 index 0000000..5ce0ff0 --- /dev/null +++ b/server/README.md @@ -0,0 +1,61 @@ +# Server Json per condivisione delle mie apps + +Utilizza MongoDB su 192.168.1.3 con user root e password example + +## Installazione ed avvio server + +vai su server e installa i packages + +```sh +cd server +npm ci install +``` + +far partire il server con + +``` +node index.js +``` +o con +``` +npm start +``` + +è settato per far partire su porta 3000 + +## User interface per inserire i dati + +vai su frontend ed avvia la UI + +``` +cd frontend +npx http-server . -c-1 -p 8282 +``` + +il comando -c-1 toglie la cache +-p indica la porta + +## Altri strumenti per l'utilizzo + +nella directory server c'è + + ./link.sh + +che estrae la lista usando curl + +oppure il comando in js + + node list.js + +che estrae la lista + +nel folder how_use le api e i vari comandi in js + +## Installazione in docker con mongoDB incluso (non testato) + +Come avviarlo + +``` +cd project +docker-compose up --build +``` diff --git a/server/backend/.env b/server/backend/.env new file mode 100644 index 0000000..2e946e6 --- /dev/null +++ b/server/backend/.env @@ -0,0 +1,16 @@ +# === SERVER CONFIG === +PORT=11001 + +# === JWT CONFIG === +# Cambialo SEMPRE in produzione +JWT_SECRET=master66 + +# === MONGO CONFIG === +# In locale: +# MONGO_URI=mongodb://localhost:27017/mydb +# +# In Docker (usato dal docker-compose): +MONGO_URI=mongodb://root:example@192.168.1.3:27017/myapphttps?authSource=admin +# === UPLOADS === +# Cartella dove Express serve le icone +UPLOAD_DIR=uploads diff --git a/server/backend/.env.example b/server/backend/.env.example new file mode 100644 index 0000000..949fb60 --- /dev/null +++ b/server/backend/.env.example @@ -0,0 +1,17 @@ +# === SERVER CONFIG === +PORT=3000 + +# === JWT CONFIG === +# Cambialo SEMPRE in produzione +JWT_SECRET=supersegreto-cambialo + +# === MONGO CONFIG === +# In locale: +# MONGO_URI=mongodb://localhost:27017/mydb +# +# In Docker (usato dal docker-compose): +MONGO_URI=mongodb://mongo:27017/mydb + +# === UPLOADS === +# Cartella dove Express serve le icone +UPLOAD_DIR=uploads diff --git a/server/backend/Dockerfile b/server/backend/Dockerfile new file mode 100644 index 0000000..acb49c9 --- /dev/null +++ b/server/backend/Dockerfile @@ -0,0 +1,38 @@ +# ----------------------------- +# 1) Build stage +# ----------------------------- +FROM node:20-alpine AS builder + +WORKDIR /app + +# Copio solo package.json per sfruttare la cache Docker +COPY package*.json ./ + +# Installa solo le dipendenze necessarie +RUN npm ci --only=production + +# Copio il resto del codice +COPY . . + +# ----------------------------- +# 2) Runtime stage +# ----------------------------- +FROM node:20-alpine + +WORKDIR /app + +# Copio solo node_modules dal builder +COPY --from=builder /app/node_modules ./node_modules + +# Copio il codice applicativo +COPY --from=builder /app ./ + +# Utente non-root per sicurezza +RUN addgroup -S appgroup && adduser -S appuser -G appgroup +USER appuser + +# Porta interna del backend (3000) +EXPOSE 11001 + +# Avvio del server +CMD ["node", "index.js"] 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 new file mode 100644 index 0000000..14b66a9 --- /dev/null +++ b/server/backend/index.js @@ -0,0 +1,57 @@ +import express from "express"; +import mongoose from "mongoose"; +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(); + +const app = express(); + +app.use(cors()); +app.use(express.json()); + +// Static folder per le icone +app.use("/uploads", express.static("uploads")); + +// Auth routes +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"; + +mongoose + .connect(MONGO_URI) + .then(() => { + console.log("MongoDB connesso"); + }) + .catch(err => { + console.error("❌ Errore di connessione a MongoDB:", err.message); + process.exit(1); // termina il processo + }); + + + +const PORT = process.env.PORT || 3000; + +const server = app.listen(PORT, () => { + console.log(`API su http://localhost:${PORT}`); +}); + +server.on('error', (err) => { + if (err.code === 'EADDRINUSE') { + console.error(`❌ Porta ${PORT} già in uso. Arresto del processo.`); + process.exit(1); + } else { + console.error('Errore del server:', err); + process.exit(1); + } +}); diff --git a/server/backend/middleware/auth.js b/server/backend/middleware/auth.js new file mode 100644 index 0000000..eefd0c4 --- /dev/null +++ b/server/backend/middleware/auth.js @@ -0,0 +1,16 @@ +import jwt from "jsonwebtoken"; + +export function authMiddleware(req, res, next) { + const authHeader = req.headers.authorization || ""; + const token = authHeader.startsWith("Bearer ") ? authHeader.slice(7) : null; + + if (!token) return res.status(401).json({ error: "Token mancante" }); + + try { + const payload = jwt.verify(token, process.env.JWT_SECRET || "devsecret"); + req.userId = payload.userId; + next(); + } catch (err) { + return res.status(401).json({ error: "Token non valido" }); + } +} diff --git a/server/backend/models/Link.js b/server/backend/models/Link.js new file mode 100644 index 0000000..7d06566 --- /dev/null +++ b/server/backend/models/Link.js @@ -0,0 +1,14 @@ +import mongoose from "mongoose"; + +const LinkSchema = new mongoose.Schema({ + url: { type: String, required: true }, + name: { type: String, required: true }, + icon: { + data: { type: Buffer, required: false }, + mime: { type: String, required: false }, + size: { type: Number, required: false } + }, + owner: { type: mongoose.Schema.Types.ObjectId, ref: "User", required: true } +}); + +export default mongoose.model("Link", LinkSchema); diff --git a/server/backend/models/User.js b/server/backend/models/User.js new file mode 100644 index 0000000..fa7e608 --- /dev/null +++ b/server/backend/models/User.js @@ -0,0 +1,8 @@ +import mongoose from "mongoose"; + +const UserSchema = new mongoose.Schema({ + email: { type: String, required: true, unique: true }, + passwordHash: { type: String, required: true } +}); + +export default mongoose.model("User", UserSchema); diff --git a/server/backend/package-lock.json b/server/backend/package-lock.json new file mode 100644 index 0000000..82c9c63 --- /dev/null +++ b/server/backend/package-lock.json @@ -0,0 +1,2953 @@ +{ + "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", + "icojs": "^0.20.1", + "image-size": "^2.0.2", + "jsonwebtoken": "^9.0.3", + "mongoose": "^9.0.2", + "multer": "^2.0.2", + "sharp": "^0.34.5" + } + }, + "node_modules/@borewit/text-codec": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.2.1.tgz", + "integrity": "sha512-k7vvKPbf7J2fZ5klGRD9AeKfUvojuZIQ3BT5u7Jfv+puwXkUBUT5PVyMDfJZpy30CBDXGMgw7fguK/lpOMBvgw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@canvas/image-data": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@canvas/image-data/-/image-data-1.1.0.tgz", + "integrity": "sha512-QdObRRjRbcXGmM1tmJ+MrHcaz1MftF2+W7YI+MsphnsCrmtyfS0d5qJbk0MeSbUeyM/jCb0hmnkXPsy026L7dA==", + "license": "MIT" + }, + "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/@jimp/bmp": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/bmp/-/bmp-0.22.12.tgz", + "integrity": "sha512-aeI64HD0npropd+AR76MCcvvRaa+Qck6loCOS03CkkxGHN5/r336qTM5HPUdHKMDOGzqknuVPA8+kK1t03z12g==", + "license": "MIT", + "dependencies": { + "@jimp/utils": "^0.22.12", + "bmp-js": "^0.1.0" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/core": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/core/-/core-0.22.12.tgz", + "integrity": "sha512-l0RR0dOPyzMKfjUW1uebzueFEDtCOj9fN6pyTYWWOM/VS4BciXQ1VVrJs8pO3kycGYZxncRKhCoygbNr8eEZQA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@jimp/utils": "^0.22.12", + "any-base": "^1.1.0", + "buffer": "^5.2.0", + "exif-parser": "^0.1.12", + "file-type": "^16.5.4", + "isomorphic-fetch": "^3.0.0", + "pixelmatch": "^4.0.2", + "tinycolor2": "^1.6.0" + } + }, + "node_modules/@jimp/core/node_modules/file-type": { + "version": "16.5.4", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-16.5.4.tgz", + "integrity": "sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw==", + "license": "MIT", + "peer": true, + "dependencies": { + "readable-web-to-node-stream": "^3.0.0", + "strtok3": "^6.2.4", + "token-types": "^4.1.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, + "node_modules/@jimp/core/node_modules/strtok3": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-6.3.0.tgz", + "integrity": "sha512-fZtbhtvI9I48xDSywd/somNqgUHl2L2cstmXCCif0itOf96jeW18MBSyrLuNicYQVkvpOxkZtkzujiTJ9LW5Jw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@tokenizer/token": "^0.3.0", + "peek-readable": "^4.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@jimp/core/node_modules/token-types": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-4.2.1.tgz", + "integrity": "sha512-6udB24Q737UD/SDsKAHI9FCRP7Bqc9D/MQUV02ORQg5iskjtLJlZJNdN4kKtcdtwCeWIwIHDGaUsTsCCAa8sFQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@jimp/custom": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/custom/-/custom-0.22.12.tgz", + "integrity": "sha512-xcmww1O/JFP2MrlGUMd3Q78S3Qu6W3mYTXYuIqFq33EorgYHV/HqymHfXy9GjiCJ7OI+7lWx6nYFOzU7M4rd1Q==", + "license": "MIT", + "peer": true, + "dependencies": { + "@jimp/core": "^0.22.12" + } + }, + "node_modules/@jimp/utils": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@jimp/utils/-/utils-0.22.12.tgz", + "integrity": "sha512-yJ5cWUknGnilBq97ZXOyOS0HhsHOyAyjHwYfHxGbSyMTohgQI6sVyE8KPgDwH8HHW/nMKXk8TrSwAE71zt716Q==", + "license": "MIT", + "dependencies": { + "regenerator-runtime": "^0.13.3" + } + }, + "node_modules/@mongodb-js/saslprep": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.4.4.tgz", + "integrity": "sha512-p7X/ytJDIdwUfFL/CLOhKgdfJe1Fa8uw9seJYvdOmnP9JBWGWHW69HkOixXS6Wy9yvGf1MbhcS6lVmrhy4jm2g==", + "license": "MIT", + "dependencies": { + "sparse-bitfield": "^3.0.3" + } + }, + "node_modules/@tokenizer/inflate": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.4.1.tgz", + "integrity": "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "token-types": "^6.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@tokenizer/token": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", + "license": "MIT" + }, + "node_modules/@types/webidl-conversions": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", + "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==", + "license": "MIT" + }, + "node_modules/@types/whatwg-url": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-13.0.0.tgz", + "integrity": "sha512-N8WXpbE6Wgri7KUSvrmQcqrMllKZ9uxkYWMt+mCSGwNc0Hsw9VQTW7ApqI4XNrx6/SaM2QQJCzMPDEXE058s+Q==", + "license": "MIT", + "dependencies": { + "@types/webidl-conversions": "*" + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "peer": true, + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/any-base": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/any-base/-/any-base-1.1.0.tgz", + "integrity": "sha512-uMgjozySS8adZZYePpaWs8cxB9/kdzmpX6SgJZ+wbz1K5eYk5QMYDVJaZKhxyIHUdnnJkfR7SVgStgH7LkGUyg==", + "license": "MIT", + "peer": true + }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "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/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "peer": true + }, + "node_modules/bcryptjs": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz", + "integrity": "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==", + "license": "BSD-3-Clause", + "bin": { + "bcrypt": "bin/bcrypt" + } + }, + "node_modules/bmp-js": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/bmp-js/-/bmp-js-0.1.0.tgz", + "integrity": "sha512-vHdS19CnY3hwiNdkaqk93DvjVLfbEcI8mys4UjuWrlX1haDmroo8o4xCzh4wD6DGV6HxRCyauwhHRqMTfERtjw==", + "license": "MIT" + }, + "node_modules/body-parser": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.1.tgz", + "integrity": "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "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", + "integrity": "sha512-Kwc6Wh4lQ5OmkqqKhYGKIuELXl+EPYSCObVE6bWsp1T/cGkOCBN0I8wF/T44BiuhHyNi1mmKVPXk60d41xZ7kw==", + "license": "Apache-2.0", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "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", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "engines": [ + "node >= 6.0" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/content-disposition": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "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", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decode-bmp": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/decode-bmp/-/decode-bmp-0.2.1.tgz", + "integrity": "sha512-NiOaGe+GN0KJqi2STf24hfMkFitDUaIoUU3eKvP/wAbLe8o6FuW5n/x7MHPR0HKvBokp6MQY/j7w8lewEeVCIA==", + "license": "MIT", + "dependencies": { + "@canvas/image-data": "^1.0.0", + "to-data-view": "^1.1.0" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/decode-bmp/node_modules/to-data-view": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/to-data-view/-/to-data-view-1.1.0.tgz", + "integrity": "sha512-1eAdufMg6mwgmlojAx3QeMnzB/BTVp7Tbndi3U7ftcT2zCZadjxkkmLmd97zmaxWi+sgGcgWrokmpEoy0Dn0vQ==", + "license": "MIT" + }, + "node_modules/decode-ico": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/decode-ico/-/decode-ico-0.4.1.tgz", + "integrity": "sha512-69NZfbKIzux1vBOd31al3XnMnH+2mqDhEgLdpygErm4d60N+UwA5Sq5WFjmEDQzumgB9fElojGwWG0vybVfFmA==", + "license": "MIT", + "dependencies": { + "@canvas/image-data": "^1.0.0", + "decode-bmp": "^0.2.0", + "to-data-view": "^1.1.0" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/decode-ico/node_modules/to-data-view": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/to-data-view/-/to-data-view-1.1.0.tgz", + "integrity": "sha512-1eAdufMg6mwgmlojAx3QeMnzB/BTVp7Tbndi3U7ftcT2zCZadjxkkmLmd97zmaxWi+sgGcgWrokmpEoy0Dn0vQ==", + "license": "MIT" + }, + "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", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "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", + "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "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", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "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", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/exif-parser": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/exif-parser/-/exif-parser-0.1.12.tgz", + "integrity": "sha512-c2bQfLNbMzLPmzQuOr8fy0csy84WmwnER81W88DzTp9CYNPJ6yzOj2EZAh9pywYpqHnshVLHQJ8WzldAyfY+Iw==", + "peer": true + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/file-type": { + "version": "21.3.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-21.3.0.tgz", + "integrity": "sha512-8kPJMIGz1Yt/aPEwOsrR97ZyZaD1Iqm8PClb1nYFclUCkBi0Ma5IsYNQzvSFS9ib51lWyIw5mIT9rWzI/xjpzA==", + "license": "MIT", + "dependencies": { + "@tokenizer/inflate": "^0.4.1", + "strtok3": "^10.3.4", + "token-types": "^6.1.1", + "uint8array-extras": "^1.4.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "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", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "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", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "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", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/icojs": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/icojs/-/icojs-0.20.1.tgz", + "integrity": "sha512-5CgDmAMlXmqMLFSCKTKrWK5FJl9WaA5Y8y3vtuNSas0HZx9O7dtS+xgeIu7jG8W3VQSXWAXdoGaILtF6ktjE7Q==", + "license": "MIT", + "dependencies": { + "@jimp/bmp": "^0.22.12", + "decode-ico": "^0.4.1", + "file-type": "^21.0.0", + "jpeg-js": "^0.4.4", + "pngjs": "^7.0.0", + "to-data-view": "^2.0.0" + }, + "engines": { + "node": ">=20.19.4" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.1.tgz", + "integrity": "sha512-2Tth85cXwGFHfvRgZWszZSvdo+0Xsqmw8k8ZwxScfcBneNUraK+dxRxRm24nszx80Y0TVio8kKLt5sLE7ZCLlw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "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", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/isomorphic-fetch": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz", + "integrity": "sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==", + "license": "MIT", + "peer": true, + "dependencies": { + "node-fetch": "^2.6.1", + "whatwg-fetch": "^3.4.1" + } + }, + "node_modules/jpeg-js": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.4.tgz", + "integrity": "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==", + "license": "BSD-3-Clause" + }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/kareem": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/kareem/-/kareem-3.0.0.tgz", + "integrity": "sha512-RKhaOBSPN8L7y4yAgNhDT2602G5FD6QbOIISbjN9D6mjHPeqeg7K+EB5IGSU5o81/X2Gzm3ICnAvQW3x3OP8HA==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/memory-pager": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", + "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", + "license": "MIT" + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/mongodb": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-7.0.0.tgz", + "integrity": "sha512-vG/A5cQrvGGvZm2mTnCSz1LUcbOPl83hfB6bxULKQ8oFZauyox/2xbZOoGNl+64m8VBrETkdGCDBdOsCr3F3jg==", + "license": "Apache-2.0", + "dependencies": { + "@mongodb-js/saslprep": "^1.3.0", + "bson": "^7.0.0", + "mongodb-connection-string-url": "^7.0.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@aws-sdk/credential-providers": "^3.806.0", + "@mongodb-js/zstd": "^7.0.0", + "gcp-metadata": "^7.0.1", + "kerberos": "^7.0.0", + "mongodb-client-encryption": ">=7.0.0 <7.1.0", + "snappy": "^7.3.2", + "socks": "^2.8.6" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-providers": { + "optional": true + }, + "@mongodb-js/zstd": { + "optional": true + }, + "gcp-metadata": { + "optional": true + }, + "kerberos": { + "optional": true + }, + "mongodb-client-encryption": { + "optional": true + }, + "snappy": { + "optional": true + }, + "socks": { + "optional": true + } + } + }, + "node_modules/mongodb-connection-string-url": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-7.0.0.tgz", + "integrity": "sha512-irhhjRVLE20hbkRl4zpAYLnDMM+zIZnp0IDB9akAFFUZp/3XdOfwwddc7y6cNvF2WCEtfTYRwYbIfYa2kVY0og==", + "license": "Apache-2.0", + "dependencies": { + "@types/whatwg-url": "^13.0.0", + "whatwg-url": "^14.1.0" + }, + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/mongoose": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-9.0.2.tgz", + "integrity": "sha512-+GCaqwE+X//yN9eo2M2L/n+mVti9J6vH5iQKbhD+2AArZd5iaZqK/DkmkE4S6/iYYMyVQPTXsRk7jyVOYEtJzA==", + "license": "MIT", + "dependencies": { + "kareem": "3.0.0", + "mongodb": "~7.0", + "mpath": "0.9.0", + "mquery": "6.0.0", + "ms": "2.1.3", + "sift": "17.1.3" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mongoose" + } + }, + "node_modules/mpath": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz", + "integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mquery": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/mquery/-/mquery-6.0.0.tgz", + "integrity": "sha512-b2KQNsmgtkscfeDgkYMcWGn9vZI9YoXh802VDEwE6qc50zxBFQ0Oo8ROkawbPAsXCY1/Z1yp0MagqsZStPWJjw==", + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/multer": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.0.2.tgz", + "integrity": "sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw==", + "license": "MIT", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.6.0", + "concat-stream": "^2.0.0", + "mkdirp": "^0.5.6", + "object-assign": "^4.1.1", + "type-is": "^1.6.18", + "xtend": "^4.0.2" + }, + "engines": { + "node": ">= 10.16.0" + } + }, + "node_modules/multer/node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/multer/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/multer/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/multer/node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "peer": true, + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-fetch/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT", + "peer": true + }, + "node_modules/node-fetch/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause", + "peer": true + }, + "node_modules/node-fetch/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "peer": true, + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "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", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "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", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/peek-readable": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-4.1.0.tgz", + "integrity": "sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/pixelmatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/pixelmatch/-/pixelmatch-4.0.2.tgz", + "integrity": "sha512-J8B6xqiO37sU/gkcMglv6h5Jbd9xNER7aHzpfRdNmV4IbQBzBpe4l9XmbG+xPF/znacgu2jfEw+wHffaq/YkXA==", + "license": "ISC", + "peer": true, + "dependencies": { + "pngjs": "^3.0.0" + }, + "bin": { + "pixelmatch": "bin/pixelmatch" + } + }, + "node_modules/pixelmatch/node_modules/pngjs": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-3.4.0.tgz", + "integrity": "sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pngjs": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz", + "integrity": "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==", + "license": "MIT", + "engines": { + "node": ">=14.19.0" + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "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", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readable-web-to-node-stream": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.4.tgz", + "integrity": "sha512-9nX56alTf5bwXQ3ZDipHJhusu9NTQJ/CVPtb/XHAJCXihZeitfJvIRS4GqQ/mfIoOE3IelHMrpayVrosdHBuLw==", + "license": "MIT", + "peer": true, + "dependencies": { + "readable-stream": "^4.7.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/readable-web-to-node-stream/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/readable-web-to-node-stream/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "license": "MIT", + "peer": true, + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "license": "MIT" + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "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", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sift": { + "version": "17.1.3", + "resolved": "https://registry.npmjs.org/sift/-/sift-17.1.3.tgz", + "integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==", + "license": "MIT" + }, + "node_modules/sparse-bitfield": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", + "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", + "license": "MIT", + "dependencies": { + "memory-pager": "^1.0.2" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/strtok3": { + "version": "10.3.4", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.4.tgz", + "integrity": "sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg==", + "license": "MIT", + "dependencies": { + "@tokenizer/token": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/tinycolor2": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz", + "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==", + "license": "MIT", + "peer": true + }, + "node_modules/to-data-view": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-data-view/-/to-data-view-2.0.0.tgz", + "integrity": "sha512-RGEM5KqlPHr+WVTPmGNAXNeFEmsBnlkxXaIfEpUYV0AST2Z5W1EGq9L/MENFrMMmL2WQr1wjkmZy/M92eKhjYA==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/token-types": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.1.2.tgz", + "integrity": "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==", + "license": "MIT", + "dependencies": { + "@borewit/text-codec": "^0.2.1", + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "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", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, + "node_modules/uint8array-extras": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz", + "integrity": "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "license": "BSD-2-Clause", + "engines": { + "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-fetch": { + "version": "3.6.20", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz", + "integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==", + "license": "MIT", + "peer": true + }, + "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", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + } + } +} diff --git a/server/backend/package.json b/server/backend/package.json new file mode 100644 index 0000000..642109e --- /dev/null +++ b/server/backend/package.json @@ -0,0 +1,20 @@ +{ + "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", + "icojs": "^0.20.1", + "image-size": "^2.0.2", + "jsonwebtoken": "^9.0.3", + "mongoose": "^9.0.2", + "multer": "^2.0.2", + "sharp": "^0.34.5" + }, + "scripts": { + "start": "node index.js" + } +} diff --git a/server/backend/routes/auth.js b/server/backend/routes/auth.js new file mode 100644 index 0000000..2f0d618 --- /dev/null +++ b/server/backend/routes/auth.js @@ -0,0 +1,42 @@ +import express from "express"; +import bcrypt from "bcryptjs"; +import jwt from "jsonwebtoken"; +import User from "../models/User.js"; + +const router = express.Router(); + +// Registrazione +router.post("/register", async (req, res) => { + const { email, password } = req.body; + if (!email || !password) + return res.status(400).json({ error: "Email e password richiesti" }); + + const existing = await User.findOne({ email }); + if (existing) return res.status(400).json({ error: "Email già registrata" }); + + const passwordHash = await bcrypt.hash(password, 10); + + const user = await User.create({ email, passwordHash }); + + res.json({ id: user._id, email: user.email }); +}); + +// Login +router.post("/login", async (req, res) => { + const { email, password } = req.body; + const user = await User.findOne({ email }); + if (!user) return res.status(400).json({ error: "Credenziali non valide" }); + + const valid = await bcrypt.compare(password, user.passwordHash); + if (!valid) return res.status(400).json({ error: "Credenziali non valide" }); + + const token = jwt.sign( + { userId: user._id }, + process.env.JWT_SECRET || "devsecret", + { expiresIn: "7d" } + ); + + res.json({ token }); +}); + +export default router; diff --git a/server/backend/routes/links.js b/server/backend/routes/links.js new file mode 100644 index 0000000..2aa9160 --- /dev/null +++ b/server/backend/routes/links.js @@ -0,0 +1,162 @@ +import express from "express"; +import multer from "multer"; +import axios from "axios"; +import sharp from "sharp"; +import Link from "../models/Link.js"; +import { authMiddleware } from "../middleware/auth.js"; +import { parseICO } from "icojs"; + +const router = express.Router(); + +// Multer in-memory (niente filesystem) +const upload = multer({ storage: multer.memoryStorage() }); + +router.use(authMiddleware); + +// Scarica immagine remota come Buffer +async function downloadImageAsBuffer(url) { + const response = await axios.get(url, { + responseType: "arraybuffer", + maxRedirects: 5, + headers: { + "User-Agent": "Mozilla/5.0", + "Accept": "image/*" + } + }); + + return { + buffer: Buffer.from(response.data), + mime: response.headers["content-type"] || "" + }; +} + +// Converte immagine → WebP 128x128 contain +async function processIcon(buffer, mime) { + let inputBuffer = buffer; + + // Se è ICO → converti in PNG + if (mime === "image/x-icon" || mime === "image/vnd.microsoft.icon") { + const images = await parseICO(buffer); + + if (!images.length) { + throw new Error("ICO non valido"); + } + + // Prendiamo l’immagine più grande dentro l’ICO + const best = images.reduce((a, b) => (a.width > b.width ? a : b)); + + inputBuffer = Buffer.from(best.buffer); + } + + // Ora Sharp può lavorare + return await sharp(inputBuffer) + .resize(128, 128, { + fit: "contain", + background: { r: 0, g: 0, b: 0, alpha: 0 } + }) + .webp({ quality: 90 }) + .toBuffer(); +} + +// =============================== +// GET LINKS +// =============================== +router.get("/", async (req, res) => { + const links = await Link.find({ owner: req.userId }); + res.json(links); +}); + +// =============================== +// CREATE LINK +// =============================== +router.post("/", upload.single("icon"), async (req, res) => { + const { url, name, iconURL } = req.body; + + let originalBuffer = null; + + // Caso 1: upload file + if (req.file) { + originalBuffer = req.file.buffer; + } + + // Caso 2: URL remoto + else if (iconURL) { + originalBuffer = await downloadImageAsBuffer(iconURL); + } + + let processedIcon = null; + + if (originalBuffer) { + processedIcon = await processIcon(originalBuffer.buffer, originalBuffer.mime); + } + + + const link = await Link.create({ + url, + name, + owner: req.userId, + icon: processedIcon + ? { + data: processedIcon, + mime: "image/webp", + size: processedIcon.length + } + : null + }); + + res.json(link); +}); + +// =============================== +// UPDATE LINK +// =============================== +router.put("/:id", upload.single("icon"), async (req, res) => { + const { id } = req.params; + const { name, url, iconURL } = req.body; + + const link = await Link.findOne({ _id: id, owner: req.userId }); + if (!link) return res.status(404).json({ error: "Link non trovato" }); + + let originalBuffer = null; + + if (req.file) { + originalBuffer = req.file.buffer; + } else if (iconURL) { + originalBuffer = await downloadImageAsBuffer(iconURL); + } + + const update = { name, url }; + + if (originalBuffer) { + const processedIcon = await processIcon(originalBuffer.buffer, originalBuffer.mime); + update.icon = { + data: processedIcon, + mime: "image/webp", + size: processedIcon.length + }; + } + + const updated = await Link.findOneAndUpdate( + { _id: id, owner: req.userId }, + update, + { new: true } + ); + + res.json(updated); +}); + +// =============================== +// DELETE LINK +// =============================== +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({ 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/1767548820927-1000084863.jpg b/server/backend/uploads/1767548820927-1000084863.jpg new file mode 100644 index 0000000..2d04eaf Binary files /dev/null and b/server/backend/uploads/1767548820927-1000084863.jpg differ diff --git a/server/backend/uploads/1767548868131.png b/server/backend/uploads/1767548868131.png new file mode 100644 index 0000000..a59308e Binary files /dev/null and b/server/backend/uploads/1767548868131.png differ diff --git a/server/frontend/api.js b/server/frontend/api.js new file mode 100644 index 0000000..c71723b --- /dev/null +++ b/server/frontend/api.js @@ -0,0 +1,122 @@ +const API_BASE = "https://myapps_svr.patachina2.casacam.net"; + +// ------------------------------ +// AUTH +// ------------------------------ + +export async function login(email, password) { + const res = await fetch(`${API_BASE}/auth/login`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ email, password }) + }); + + const data = await res.json(); + if (!res.ok) throw new Error(data.error || "Errore login"); + return data.token; +} + +export async function register(email, password) { + const res = await fetch(`${API_BASE}/auth/register`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ email, password }) + }); + + const data = await res.json(); + if (!res.ok) throw new Error(data.error || "Errore registrazione"); + return data; +} + +// ------------------------------ +// LINKS +// ------------------------------ + +export async function getLinks(token) { + const res = await fetch(`${API_BASE}/links`, { + headers: { + Authorization: `Bearer ${token}`, + Accept: "application/json" + } + }); + + const data = await res.json(); + if (!res.ok) throw new Error(data.error || "Errore caricamento link"); + return data; +} + +/*export async function createLink(token, { name, url, iconFile }) { + const formData = new FormData(); + formData.append("name", name); + formData.append("url", url); + if (iconFile) formData.append("icon", iconFile); + + 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; +}*/ + +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; +} + +export async function deleteLink(token, id) { + const res = await fetch(`${API_BASE}/links/${id}`, { + method: "DELETE", + headers: { + Authorization: `Bearer ${token}` + } + }); + + const data = await res.json(); + if (!res.ok) throw new Error(data.error || "Errore eliminazione link"); + return data; +} + +export async function updateLink(token, id, { name, url, iconFile }) { + const formData = new FormData(); + if (name) formData.append("name", name); + if (url) formData.append("url", url); + if (iconFile) formData.append("icon", iconFile); + + const res = await fetch(`${API_BASE}/links/${id}`, { + method: "PUT", + headers: { + Authorization: `Bearer ${token}` + }, + body: formData + }); + + const data = await res.json(); + if (!res.ok) throw new Error(data.error || "Errore aggiornamento link"); + return data; +} + diff --git a/server/frontend/app.js b/server/frontend/app.js new file mode 100644 index 0000000..bd71689 --- /dev/null +++ b/server/frontend/app.js @@ -0,0 +1,273 @@ +import { + login, + register, + getLinks, + createLink, + deleteLink, + updateLink +} from "./api.js"; + +const URL_SVR = "https://myapps_svr.patachina2.casacam.net"; + +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; + 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(); + try { + const t = await login(e.target.email.value, e.target.password.value); + setToken(t); + } catch (err) { + document.getElementById("authStatus").textContent = err.message; + } +}); + +document.getElementById("registerForm").addEventListener("submit", async e => { + e.preventDefault(); + try { + await register(e.target.email.value, e.target.password.value); + document.getElementById("authStatus").textContent = "Registrato! Ora accedi."; + } catch (err) { + document.getElementById("authStatus").textContent = err.message; + } +}); + +// =============================== +// LOAD LINKS +// =============================== +/*async function loadLinks() { + const links = await getLinks(token); + const list = document.getElementById("list"); + + list.innerHTML = links + .map( + link => ` +
+ ${link.icon ? `` : ""} +
+ ${link.name}
+ ${link.url} +
+
+ + +
+
+ ` + ) + .join(""); +}*/ +async function loadLinks() { + const links = await getLinks(token); + alert(links); + const list = document.getElementById("list"); + +list.innerHTML = links + .map(link => { + let iconHtml = ""; + + if (link.icon && link.icon.data && link.icon.mime) { + const base64 = btoa( + String.fromCharCode(...link.icon.data.data) + ); + iconHtml = ``; + } + + return ` +
+ ${iconHtml} +
+ ${link.name}
+ ${link.url} +
+
+ + +
+
+ `; + }) + .join(""); + +/* + list.innerHTML = links + .map(link => ` +
+ ${link.icon ? `` : ""} +
+ ${link.name}
+ ${link.url} +
+
+ + +
+
+ `) + .join("");*/ +} + +// =============================== +// 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 raw = new FormData(e.target); + const manualFile = raw.get("icon"); + const hasManualFile = manualFile instanceof File && manualFile.size > 0; + + await createLink(token, { + 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(); +}); + +// =============================== +// EDIT +// =============================== +document.getElementById("list").addEventListener("click", e => { + const id = e.target.dataset.id; + if (!id) return; + + if (e.target.classList.contains("deleteBtn")) { + deleteLink(token, id).then(loadLinks); + return; + } + + if (e.target.classList.contains("editBtn")) { + editingId = id; + + const item = e.target.closest(".item"); + const name = item.querySelector("strong").textContent; + const url = item.querySelector("a").textContent; + + const form = document.getElementById("editForm"); + form.name.value = name; + form.url.value = url; + + document.getElementById("iconPreviewEdit").style.display = "none"; + document.getElementById("iconSizeEdit").style.display = "none"; + + document.getElementById("editModal").style.display = "flex"; + } +}); + +// 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 = 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, + iconURL: null + }); + + document.getElementById("editModal").style.display = "none"; + loadLinks(); +}); + +document.getElementById("closeModal").addEventListener("click", () => { + document.getElementById("editModal").style.display = "none"; +}); + +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 new file mode 100644 index 0000000..0fbdb07 --- /dev/null +++ b/server/frontend/index.html @@ -0,0 +1,92 @@ + + + + + Link Manager + + + + +
+ +

Link Manager

+ + +
+
+

Accedi

+
+ + + +
+ +

Oppure registrati

+
+ + + +
+ +
+
+
+ + + + +
+ + + + + + + + + + diff --git a/server/frontend/start.sh b/server/frontend/start.sh new file mode 100755 index 0000000..65193b5 --- /dev/null +++ b/server/frontend/start.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +# Avvia un server HTTP sulla porta 11002 senza cache +npx http-server . -c-1 -p 11003 diff --git a/server/frontend/style.css b/server/frontend/style.css new file mode 100644 index 0000000..5e2c3c3 --- /dev/null +++ b/server/frontend/style.css @@ -0,0 +1,85 @@ +body { + font-family: -apple-system, BlinkMacSystemFont, "SF Pro", sans-serif; + background: #f5f5f7; + margin: 0; + padding: 40px; + color: #333; +} + +.container { + max-width: 700px; + margin: auto; +} + +h1 { + text-align: center; + margin-bottom: 40px; + font-weight: 600; +} + +.card { + background: white; + padding: 25px; + border-radius: 18px; + box-shadow: 0 4px 20px rgba(0,0,0,0.08); + margin-bottom: 30px; +} + +form { + display: flex; + flex-direction: column; + gap: 12px; +} + +input { + padding: 12px; + border-radius: 10px; + border: 1px solid #ccc; + font-size: 15px; +} + +button { + padding: 12px; + border-radius: 10px; + border: none; + background: #007aff; + color: white; + font-size: 16px; + cursor: pointer; + font-weight: 600; +} + +button:hover { + background: #0063cc; +} + +#list .item { + display: flex; + align-items: center; + gap: 12px; + padding: 10px 0; + border-bottom: 1px solid #eee; +} + +#list img { + width: 40px; + height: 40px; + object-fit: contain; + border-radius: 8px; +} +.modal { + position: fixed; + top: 0; left: 0; + width: 100%; height: 100%; + background: rgba(0,0,0,0.4); + display: flex; + justify-content: center; + align-items: center; +} + +.modal-content { + background: white; + padding: 20px; + border-radius: 12px; + width: 300px; +} diff --git a/varie/a.js b/varie/a.js new file mode 100644 index 0000000..450eabe --- /dev/null +++ b/varie/a.js @@ -0,0 +1,16 @@ +// a.js +import { getAppMetadata } from "./appMetadata.js"; + +async function main() { + const url = process.argv[2]; + + if (!url) { + console.log("Uso: node a.js "); + process.exit(1); + } + + const result = await getAppMetadata(url); + console.log(result); +} + +main(); diff --git a/varie/l b/varie/l new file mode 100755 index 0000000..726fefa --- /dev/null +++ b/varie/l @@ -0,0 +1,29 @@ +#!/bin/bash + +API_URL="http://192.168.1.3:3000" +EMAIL="fabio.micheluz@gmail.com" +PASSWORD="master66" + +echo "➡️ Effettuo login..." + +# Login e estrazione token +TOKEN=$(curl -s -X POST "$API_URL/auth/login" \ + -H "Content-Type: application/json" \ + -d "{\"email\":\"$EMAIL\",\"password\":\"$PASSWORD\"}" | jq -r '.token') + +# Controllo token +if [ "$TOKEN" == "null" ] || [ -z "$TOKEN" ]; then + echo "❌ Errore: impossibile ottenere il token. Controlla email/password." + exit 1 +fi + +echo "🔑 Token ottenuto." + +echo "➡️ Richiedo lista link..." + +# Richiesta protetta +curl -s -X GET "$API_URL/links" \ + -H "Authorization: Bearer $TOKEN" \ + -H "Accept: application/json" | jq . + +echo "✅ Fine." diff --git a/varie/l.sh b/varie/l.sh new file mode 100755 index 0000000..9c38ac6 --- /dev/null +++ b/varie/l.sh @@ -0,0 +1,29 @@ +#!/bin/bash + +API_URL="https://myapps_svr.patachina2.casacam.net" +EMAIL="fabio.micheluz@gmail.com" +PASSWORD="master66" + +echo "➡️ Effettuo login..." + +# Login e estrazione token +TOKEN=$(curl -s -X POST "$API_URL/auth/login" \ + -H "Content-Type: application/json" \ + -d "{\"email\":\"$EMAIL\",\"password\":\"$PASSWORD\"}" | jq -r '.token') + +# Controllo token +if [ "$TOKEN" == "null" ] || [ -z "$TOKEN" ]; then + echo "❌ Errore: impossibile ottenere il token. Controlla email/password." + exit 1 +fi + +echo "🔑 Token ottenuto." + +echo "➡️ Richiedo lista link..." + +# Richiesta protetta +curl -s -X GET "$API_URL/links" \ + -H "Authorization: Bearer $TOKEN" \ + -H "Accept: application/json" | jq . + +echo "✅ Fine." diff --git a/varie/list.js b/varie/list.js new file mode 100644 index 0000000..aa2db94 --- /dev/null +++ b/varie/list.js @@ -0,0 +1,26 @@ +async function login(email, password) { + const res = await fetch("http://192.168.1.3:3000/auth/login", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ email, password }) + }); + + const data = await res.json(); + return data.token; +} + +async function getLinks() { + const token = await login("fabio.micheluz@gmail.com", "master66"); + + const res = await fetch("http://192.168.1.3:3000/links", { + headers: { + "Authorization": `Bearer ${token}`, + "Accept": "application/json" + } + }); + + const json = await res.json(); + console.log(json); +} + +getLinks(); diff --git a/varie/list.sh b/varie/list.sh new file mode 100755 index 0000000..726fefa --- /dev/null +++ b/varie/list.sh @@ -0,0 +1,29 @@ +#!/bin/bash + +API_URL="http://192.168.1.3:3000" +EMAIL="fabio.micheluz@gmail.com" +PASSWORD="master66" + +echo "➡️ Effettuo login..." + +# Login e estrazione token +TOKEN=$(curl -s -X POST "$API_URL/auth/login" \ + -H "Content-Type: application/json" \ + -d "{\"email\":\"$EMAIL\",\"password\":\"$PASSWORD\"}" | jq -r '.token') + +# Controllo token +if [ "$TOKEN" == "null" ] || [ -z "$TOKEN" ]; then + echo "❌ Errore: impossibile ottenere il token. Controlla email/password." + exit 1 +fi + +echo "🔑 Token ottenuto." + +echo "➡️ Richiedo lista link..." + +# Richiesta protetta +curl -s -X GET "$API_URL/links" \ + -H "Authorization: Bearer $TOKEN" \ + -H "Accept: application/json" | jq . + +echo "✅ Fine." diff --git a/varie/list1.js b/varie/list1.js new file mode 100644 index 0000000..0ff16b5 --- /dev/null +++ b/varie/list1.js @@ -0,0 +1,30 @@ +const URI = "https://my.patachina2.casacam.net"; +const USER = "fabio.micheluz@gmail.com"; +const PASSW = "master66"; + +async function login(email, password) { + const res = await fetch(`${URI}/auth/login`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ email, password }) + }); + + const data = await res.json(); + return data.token; +} + +async function getLinks() { + const token = await login(USER, PASSW); + + const res = await fetch(`${URI}/links`, { + headers: { + "Authorization": `Bearer ${token}`, + "Accept": "application/json" + } + }); + + const json = await res.json(); + console.log(json); +} + +getLinks();