commit 179d2069109bf8fe38c0f5e2f356e12828073aa2 Author: Fabio Date: Thu Feb 26 11:48:04 2026 +0100 first commit diff --git a/.env b/.env new file mode 100644 index 0000000..a218aec --- /dev/null +++ b/.env @@ -0,0 +1,6 @@ +BASE_URL=https://prova.patachina.it +SERVER_PORT=4000 +EMAIL=fabio@gmail.com +PASSWORD=master66 +JWT_SECRET=123456789 +JWT_EXPIRES=1h diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3eaf945 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +thumbs/ +db.json diff --git a/README.md b/README.md new file mode 100644 index 0000000..3a0f628 --- /dev/null +++ b/README.md @@ -0,0 +1,69 @@ +# Galleria con json-server e protetto con JWT + +## Installazione + +clonare questa repo e installare tutte le dipendenze con `npm ci` + + +## Start/Stop servers + +| Description | Script | +| ------------------------- | -------------------- | +| Start server senza auth | `npm start-no-auth` | +| Start server con auth | `npm run start` | + +## Tools + +| Description | Script | +| ------------------------------ | ------------------- | +| Generate user hashed passwords | `npm run hash` | + + +[json-server api reference](https://github.com/typicode/json-server) + +## Come usarlo + +clonare e poi installare con + +``` +npm ci +``` + +nel file .env ci sono tutti i dati da modificare + +poi inserire in user.json user e password utilizzati per fare il login + +la password da inserire è criptata e viene generata con npm run hash + +il nome viene utilizzato come cartella da scansionare, si trova dentro photos + +es: +``` +name: Fabio + +public/photos +└── Fabio + └── original + └── 2017Irlanda19-29ago + ├── IMG_0092.JPG + ├── IMG_0099.JPG + ├── IMG_0100.JPG +``` +poi dentro Fabio genererà thumbs con tutti i thumbs + +- npm run start +- su IP:4000 ci sarà la galleria e andando su impostazioni si potrà fare lo scan di tutte le foto + +dopo aver fatto lo scan è possibile richiedere il json al server con tutte le informazioni anche senza autorizzazione + +basta farlo partire con npm run start-no-auth e le info si possono vedere con + +ip:4000/photos + +- npm start + + +--- + +Inspired in this [post](https://www.techiediaries.com/fake-api-jwt-json-server/) by [Techiediaries](https://www.techiediaries.com/) + diff --git a/api_v1/geo.js b/api_v1/geo.js new file mode 100644 index 0000000..387f081 --- /dev/null +++ b/api_v1/geo.js @@ -0,0 +1,96 @@ +const axios = require("axios"); + +// Funzione principale +async function loc(lng, lat) { + const primary = await place(lng, lat); // Geoapify + const fallback = await placePhoton(lng, lat); // Photon + + // Se Geoapify fallisce → usa Photon + if (!primary) return fallback; + + // Se Geoapify manca city → prendi da Photon + if (!primary.city && fallback?.city) { + primary.city = fallback.city; + } + + // Se Geoapify manca postcode → prendi da Photon + if (!primary.postcode && fallback?.postcode) { + primary.postcode = fallback.postcode; + } + + // Se Geoapify manca address → prendi da Photon + if (!primary.address && fallback?.address) { + primary.address = fallback.address; + } + + // Se Geoapify manca region → prendi da Photon + if (!primary.region && fallback?.region) { + primary.region = fallback.region; + } + + // Se Geoapify manca county_code → Photon NON lo fornisce + // quindi non possiamo riempirlo + + return primary; +} + +// Geoapify (sorgente principale) +async function place(lng, lat) { + const apiKey = "6dc7fb95a3b246cfa0f3bcef5ce9ed9a"; + const url = `https://api.geoapify.com/v1/geocode/reverse?lat=${lat}&lon=${lng}&apiKey=${apiKey}`; + + try { + const r = await axios.get(url); + + if (r.status !== 200) return undefined; + if (!r.data.features || r.data.features.length === 0) return undefined; + + const k = r.data.features[0].properties; + + return { + continent: k?.timezone?.name?.split("/")?.[0] || undefined, + country: k?.country || undefined, + region: k?.state || undefined, + postcode: k?.postcode || undefined, + city: k?.city || undefined, + county_code: k?.county_code || undefined, + address: k?.address_line1 || undefined, + timezone: k?.timezone?.name || undefined, + time: k?.timezone?.offset_STD || undefined + }; + + } catch (err) { + return undefined; + } +} + +// Photon (fallback) +async function placePhoton(lng, lat) { + try { + const url = `https://photon.patachina.it/reverse?lon=${lng}&lat=${lat}`; + const r = await axios.get(url); + + if (!r.data || !r.data.features || r.data.features.length === 0) { + return undefined; + } + + const p = r.data.features[0].properties; + + return { + continent: undefined, // Photon non lo fornisce + country: p.country || undefined, + region: p.state || undefined, + postcode: p.postcode || undefined, + city: p.city || p.town || p.village || undefined, + county_code: undefined, // Photon non fornisce codici ISO + address: p.street ? `${p.street} ${p.housenumber || ""}`.trim() : undefined, + timezone: undefined, + time: undefined + }; + + } catch (err) { + return undefined; + } +} + +module.exports = loc; diff --git a/api_v1/initialDB.json b/api_v1/initialDB.json new file mode 100644 index 0000000..5a4f5e9 --- /dev/null +++ b/api_v1/initialDB.json @@ -0,0 +1 @@ +{"photos":[]} \ No newline at end of file diff --git a/api_v1/scanphoto.js b/api_v1/scanphoto.js new file mode 100644 index 0000000..94987de --- /dev/null +++ b/api_v1/scanphoto.js @@ -0,0 +1,381 @@ +/* eslint-disable no-console */ +require('dotenv').config(); + +const fs = require('fs'); +const fsp = require('fs/promises'); +const path = require('path'); +const crypto = require('crypto'); +const ExifReader = require('exifreader'); +const sharp = require('sharp'); +const axios = require('axios'); +const { exec } = require('child_process'); + +// IMPORT GEO.JS +const loc = require('./geo.js'); + +const BASE_URL = process.env.BASE_URL; +const EMAIL = process.env.EMAIL; +const PASSWORD = process.env.PASSWORD; + +const SEND_PHOTOS = (process.env.SEND_PHOTOS || 'true').toLowerCase() === 'true'; +const WRITE_INDEX = (process.env.WRITE_INDEX || 'true').toLowerCase() === 'true'; +const WEB_ROOT = process.env.WEB_ROOT || 'public'; +const INDEX_PATH = process.env.INDEX_PATH || path.posix.join('photos', 'index.json'); + +const SUPPORTED_EXTS = new Set([ + '.jpg', '.jpeg', '.png', '.webp', '.heic', '.heif', + '.mp4', '.mov', '.m4v' +]); + +// ----------------------------------------------------------------------------- +// UTILS +// ----------------------------------------------------------------------------- + +function toPosix(p) { + return p.split(path.sep).join('/'); +} + +function sha256(s) { + return crypto.createHash('sha256').update(s).digest('hex'); +} + +function inferMimeFromExt(ext) { + switch (ext.toLowerCase()) { + case '.jpg': + case '.jpeg': return 'image/jpeg'; + case '.png': return 'image/png'; + case '.webp': return 'image/webp'; + case '.heic': + case '.heif': return 'image/heic'; + case '.mp4': return 'video/mp4'; + case '.mov': return 'video/quicktime'; + case '.m4v': return 'video/x-m4v'; + default: return 'application/octet-stream'; + } +} + +function parseExifDateUtc(s) { + if (!s) return null; + const re = /^(\d{4}):(\d{2}):(\d{2}) (\d{2}):(\d{2}):(\d{2})$/; + const m = re.exec(s); + if (!m) return null; + const dt = new Date(Date.UTC(+m[1], +m[2]-1, +m[3], +m[4], +m[5], +m[6])); + return dt.toISOString(); +} + +// ----------------------------------------------------------------------------- +// GPS — FOTO +// ----------------------------------------------------------------------------- + +function extractGpsFromExif(tags) { + if (!tags?.gps) return null; + + const lat = tags.gps.Latitude; + const lng = tags.gps.Longitude; + const alt = tags.gps.Altitude; + + if (lat == null || lng == null) return null; + + return { + lat: Number(lat), + lng: Number(lng), + alt: alt != null ? Number(alt) : null + }; +} + +// ----------------------------------------------------------------------------- +// GPS — VIDEO (exiftool) +// ----------------------------------------------------------------------------- + +function extractGpsWithExiftool(videoPath) { + return new Promise((resolve) => { + const cmd = `exiftool -n -G1 -a -gps:all -quicktime:all -user:all "${videoPath}"`; + exec(cmd, (err, stdout) => { + + if (err || !stdout) return resolve(null); + + const userData = stdout.match(/GPS Coordinates\s*:\s*([0-9\.\-]+)\s+([0-9\.\-]+)/i); + if (userData) { + return resolve({ + lat: Number(userData[1]), + lng: Number(userData[2]), + alt: null + }); + } + + const lat1 = stdout.match(/GPSLatitude\s*:\s*([0-9\.\-]+)/i); + const lng1 = stdout.match(/GPSLongitude\s*:\s*([0-9\.\-]+)/i); + if (lat1 && lng1) { + return resolve({ + lat: Number(lat1[1]), + lng: Number(lng1[1]), + alt: null + }); + } + + const coords = stdout.match(/GPSCoordinates\s*:\s*([0-9\.\-]+)\s+([0-9\.\-]+)/i); + if (coords) { + return resolve({ + lat: Number(coords[1]), + lng: Number(coords[2]), + alt: null + }); + } + + resolve(null); + }); + }); +} + +// ----------------------------------------------------------------------------- +// AUTH / POST +// ----------------------------------------------------------------------------- + +let cachedToken = null; + +async function getToken(force = false) { + if (!SEND_PHOTOS) return null; + if (cachedToken && !force) return cachedToken; + + try { + const res = await axios.post(`${BASE_URL}/auth/login`, { + email: EMAIL, + password: PASSWORD + }); + + cachedToken = res.data.token; + return cachedToken; + + } catch (err) { + console.error('ERRORE LOGIN:', err.message); + return null; + } +} + +async function postWithAuth(url, payload) { + if (!SEND_PHOTOS) return; + + let token = await getToken(); + if (!token) throw new Error('Token assente'); + + try { + await axios.post(url, payload, { + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json' + }, + timeout: 20000, + }); + + } catch (err) { + if (err.response && err.response.status === 401) { + token = await getToken(true); + if (!token) throw err; + + await axios.post(url, payload, { + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json' + }, + timeout: 20000, + }); + + } else { + throw err; + } + } +} + +// ----------------------------------------------------------------------------- +// VIDEO: ffmpeg thumbnail +// ----------------------------------------------------------------------------- + +function createVideoThumbnail(videoPath, thumbMinPath, thumbAvgPath) { + return new Promise((resolve) => { + const cmd = ` + ffmpeg -y -i "${videoPath}" -ss 00:00:01.000 -vframes 1 "${thumbAvgPath}" && + ffmpeg -y -i "${thumbAvgPath}" -vf "scale=100:-1" "${thumbMinPath}" + `; + + exec(cmd, () => resolve()); + }); +} + +// ----------------------------------------------------------------------------- +// VIDEO: ffprobe metadata +// ----------------------------------------------------------------------------- + +function probeVideo(videoPath) { + return new Promise((resolve) => { + const cmd = `ffprobe -v quiet -print_format json -show_format -show_streams "${videoPath}"`; + exec(cmd, (err, stdout) => { + if (err) return resolve({}); + try { + resolve(JSON.parse(stdout)); + } catch { + resolve({}); + } + }); + }); +} + +// ----------------------------------------------------------------------------- +// THUMBNAILS IMMAGINI +// ----------------------------------------------------------------------------- + +async function createThumbnails(filePath, thumbMinPath, thumbAvgPath) { + try { + await sharp(filePath) + .resize({ width: 100, height: 100, fit: 'inside', withoutEnlargement: true }) + .withMetadata() + .toFile(thumbMinPath); + + await sharp(filePath) + .resize({ width: 400, withoutEnlargement: true }) + .withMetadata() + .toFile(thumbAvgPath); + } catch (err) { + console.error('Errore creazione thumbnails:', err.message, filePath); + } +} + +// ----------------------------------------------------------------------------- +// SCANSIONE RICORSIVA +// ----------------------------------------------------------------------------- + +async function scanDir(dirAbs, userName, results = []) { + const dirEntries = await fsp.readdir(dirAbs, { withFileTypes: true }); + + for (const dirent of dirEntries) { + const absPath = path.join(dirAbs, dirent.name); + + if (dirent.isDirectory()) { + await scanDir(absPath, userName, results); + continue; + } + + const ext = path.extname(dirent.name).toLowerCase(); + if (!SUPPORTED_EXTS.has(ext)) continue; + console.log("Elaboro:", absPath); + const isVideo = ['.mp4', '.mov', '.m4v'].includes(ext); + + const relFile = toPosix(path.relative(WEB_ROOT, absPath)); + const relDir = toPosix(path.posix.dirname(relFile)); + + const relThumbDir = relDir.replace(/original/i, 'thumbs'); + const absThumbDir = path.join(WEB_ROOT, relThumbDir); + await fsp.mkdir(absThumbDir, { recursive: true }); + + const baseName = path.parse(dirent.name).name; + + const absThumbMin = path.join(absThumbDir, `${baseName}_min.jpg`); + const absThumbAvg = path.join(absThumbDir, `${baseName}_avg.jpg`); + + if (isVideo) { + await createVideoThumbnail(absPath, absThumbMin, absThumbAvg); + } else { + await createThumbnails(absPath, absThumbMin, absThumbAvg); + } + + const relThumbMin = toPosix(path.relative(WEB_ROOT, absThumbMin)); + const relThumbAvg = toPosix(path.relative(WEB_ROOT, absThumbAvg)); + + let tags = {}; + try { + tags = await ExifReader.load(absPath, { expanded: true }); + } catch {} + + const timeRaw = tags?.exif?.DateTimeOriginal?.value?.[0] || null; + const takenAtIso = parseExifDateUtc(timeRaw); + + let gps = null; + + if (isVideo) { + gps = await extractGpsWithExiftool(absPath); + } else { + gps = extractGpsFromExif(tags); + } + + let width = null, height = null, size_bytes = null, duration = null; + + const st = await fsp.stat(absPath); + size_bytes = st.size; + + if (isVideo) { + const info = await probeVideo(absPath); + const stream = info.streams?.find(s => s.width && s.height); + if (stream) { + width = stream.width; + height = stream.height; + } + duration = info.format?.duration || null; + } else { + try { + const meta = await sharp(absPath).metadata(); + width = meta.width || null; + height = meta.height || null; + } catch {} + } + + const mime_type = inferMimeFromExt(ext); + const id = sha256(relFile); + + const location = gps ? await loc(gps.lng, gps.lat) : null; + + results.push({ + id, + name: dirent.name, + path: relFile, + thub1: relThumbMin, + thub2: relThumbAvg, + gps, + data: timeRaw, + taken_at: takenAtIso, + mime_type, + width, + height, + size_bytes, + duration: isVideo ? duration : null, + location, + user: userName + }); + } + + return results; +} + +// ----------------------------------------------------------------------------- +// MAIN +// ----------------------------------------------------------------------------- + +async function scanPhoto(dir, userName) { + try { + const absDir = path.isAbsolute(dir) ? dir : path.join(process.cwd(), dir); + const photos = await scanDir(absDir, userName); + + if (SEND_PHOTOS && BASE_URL) { + for (const p of photos) { + try { + await postWithAuth(`${BASE_URL}/photos`, p); + } catch (err) { + console.error('Errore invio:', err.message); + } + } + } +/* + if (WRITE_INDEX) { + const absIndexPath = path.join(WEB_ROOT, INDEX_PATH); + await fsp.mkdir(path.dirname(absIndexPath), { recursive: true }); + await fsp.writeFile(absIndexPath, JSON.stringify(photos, null, 2), 'utf8'); + } +*/ + await new Promise(r => setTimeout(r, 500)); + return photos; + + } catch (e) { + console.error('Errore generale scanPhoto:', e); + throw e; + } +} + +module.exports = scanPhoto; diff --git a/api_v1/server.js b/api_v1/server.js new file mode 100644 index 0000000..7696e2a --- /dev/null +++ b/api_v1/server.js @@ -0,0 +1,255 @@ +require('dotenv').config(); + +const fs = require('fs'); +//const bodyParser = require('body-parser'); +const jsonServer = require('json-server'); +const jwt = require('jsonwebtoken'); +const bcrypt = require('bcrypt'); +const path = require('path'); +const scanPhoto = require('./scanphoto.js'); + +const SECRET_KEY = process.env.JWT_SECRET || '123456789'; +const EXPIRES_IN = process.env.JWT_EXPIRES || '1h'; +const PORT = process.env.SERVER_PORT || 4000; + +const server = jsonServer.create(); + +// ----------------------------------------------------- +// STATIC FILES +// ----------------------------------------------------- +server.use(jsonServer.defaults({ + static: path.join(__dirname, '../public') +})); + +// ----------------------------------------------------- +// CONFIG ENDPOINT (PUBBLICO) +// ----------------------------------------------------- +server.get('/config', (req, res) => { + res.json({ + baseUrl: process.env.BASE_URL + }); +}); + +// ----------------------------------------------------- +// ROUTER DB +// ----------------------------------------------------- +let router; +if (fs.existsSync('./api_v1/db.json')) { + router = jsonServer.router('./api_v1/db.json'); +} else { + const initialData = fs.readFileSync('api_v1/initialDB.json', 'utf8'); + fs.writeFileSync('api_v1/db.json', initialData); + router = jsonServer.router('./api_v1/db.json'); +} + +// ----------------------------------------------------- +// USERS DB +// ----------------------------------------------------- +const userdb = JSON.parse(fs.readFileSync('./api_v1/users.json', 'UTF-8')); + +//server.use(bodyParser.urlencoded({ extended: true })); +//server.use(bodyParser.json()); +server.use(jsonServer.bodyParser); + +// ----------------------------------------------------- +// JWT HELPERS +// ----------------------------------------------------- +function createToken(payload) { + return jwt.sign(payload, SECRET_KEY, { expiresIn: EXPIRES_IN }); +} + +// Denylist per token revocati fino a scadenza +// token(string) -> exp(secondi epoch) +const denylist = new Map(); + +function addToDenylist(token) { + try { + const decoded = jwt.decode(token); + const exp = decoded?.exp || Math.floor(Date.now() / 1000) + 60; // fallback 60s + denylist.set(token, exp); + } catch { + denylist.set(token, Math.floor(Date.now() / 1000) + 60); + } +} + +function isRevoked(token) { + const exp = denylist.get(token); + if (!exp) return false; + // pulizia automatica entry scadute + if (exp * 1000 < Date.now()) { + denylist.delete(token); + return false; + } + return true; +} + +// Versione SINCRONA: lancia errore se il token non è valido +function verifyToken(token) { + return jwt.verify(token, SECRET_KEY); +} + +function isAuthenticated({ email, password }) { + return userdb.users.findIndex( + user => user.email === email && bcrypt.compareSync(password, user.password) + ) !== -1; +} + +// ----------------------------------------------------- +// RESET DB +// ----------------------------------------------------- +function resetDB() { + const initialData = fs.readFileSync('api_v1/initialDB.json', 'utf8'); + fs.writeFileSync('api_v1/db.json', initialData); + router.db.setState(JSON.parse(initialData)); + console.log('DB resettato'); +} + +// ----------------------------------------------------- +// HOME +// ----------------------------------------------------- +server.get('/', (req, res) => { + res.sendFile(path.resolve('public/index.html')); +}); + +// ----------------------------------------------------- +// LOGIN (PUBBLICO) +// ----------------------------------------------------- +server.post('/auth/login', (req, res) => { + const { email, password } = req.body; + + const user = userdb.users.find(u => u.email === email); + + if (!user || !bcrypt.compareSync(password, user.password)) { + return res.status(401).json({ status: 401, message: 'Incorrect email or password' }); + } + + const token = createToken({ + id: user.id, + email: user.email, + name: user.name + }); + + res.status(200).json({ + token, + name: user.name + }); +}); + +// ----------------------------------------------------- +// LOGOUT (PUBBLICO - usa Authorization se presente) +// ----------------------------------------------------- +server.post('/auth/logout', (req, res) => { + const auth = req.headers.authorization || ''; + const [scheme, token] = auth.split(' '); + + if (scheme === 'Bearer' && token) { + // Revoca il token fino alla sua scadenza + addToDenylist(token); + } + // 204: nessun contenuto, logout idempotente + return res.status(204).end(); +}); + +// ----------------------------------------------------- +// JWT MIDDLEWARE (TUTTO IL RESTO È PROTETTO) +// ----------------------------------------------------- +server.use(/^(?!\/auth).*$/, (req, res, next) => { + const auth = req.headers.authorization || ''; + const [scheme, token] = auth.split(' '); + + if (scheme !== 'Bearer' || !token) { + return res.status(401).json({ status: 401, message: 'Bad authorization header' }); + } + + // Blocca token revocati (logout) + if (isRevoked(token)) { + return res.status(401).json({ status: 401, message: 'Token revoked' }); + } + + try { + const decoded = verifyToken(token); + req.user = decoded; // { id, email, name, iat, exp } + next(); + } catch (err) { + return res.status(401).json({ status: 401, message: 'Error: access_token is not valid' }); + } +}); + +// ----------------------------------------------------- +// FILTRO AUTOMATICO PER USER +// ----------------------------------------------------- +server.use((req, res, next) => { + if (req.method === 'GET' && req.user) { + req.query.user = req.user.name; + } + next(); +}); + +// ----------------------------------------------------- +// SCAN FOTO (PER-UTENTE) +// ----------------------------------------------------- +server.get('/scan', async (req, res) => { + try { + resetDB(); + + const userFolder = `./public/photos/${req.user.name}/original`; + + await scanPhoto(userFolder, req.user.name); + + res.send({ + status: 'Ricaricato', + user: req.user.name, + folder: userFolder + }); + } catch (err) { + console.error('Errore scan:', err); + res.status(500).json({ error: 'Errore durante lo scan', details: err.message }); + } +}); + +// ----------------------------------------------------- +// FILE STATICI (hardening path traversal) +// ----------------------------------------------------- +server.get('/files', (req, res) => { + const requested = req.query.file || ''; + const publicDir = path.resolve(path.join(__dirname, '../public')); + const resolved = path.resolve(publicDir, requested); + + if (!resolved.startsWith(publicDir)) { + return res.status(400).json({ error: 'Invalid path' }); + } + + if (!fs.existsSync(resolved) || fs.statSync(resolved).isDirectory()) { + return res.status(404).json({ error: 'Not found' }); + } + + res.sendFile(resolved); +}); + +// ----------------------------------------------------- +// RESET DB MANUALE +// ----------------------------------------------------- +server.get('/initDB', (req, res) => { + resetDB(); + res.send({ status: 'DB resettato' }); +}); + +// ----------------------------------------------------- +// ROUTER JSON-SERVER +// ----------------------------------------------------- +server.use(router); + +// ----------------------------------------------------- +// START SERVER +// ----------------------------------------------------- +server.listen(PORT, () => { + console.log(`Auth API server running on port ${PORT} ...`); +}); + +// (Opzionale) pulizia periodica denylist per contenere la memoria +setInterval(() => { + const nowSec = Math.floor(Date.now() / 1000); + for (const [tok, exp] of denylist.entries()) { + if (exp < nowSec) denylist.delete(tok); + } +}, 60 * 1000); diff --git a/api_v1/tools.js b/api_v1/tools.js new file mode 100644 index 0000000..13fca1d --- /dev/null +++ b/api_v1/tools.js @@ -0,0 +1,51 @@ +/** + * Required libraries + */ +const bcrypt = require('bcrypt') +const readLine = require('readline') +const async = require('async') + +// Password hash method +const hashPassword = plain => bcrypt.hashSync(plain, 8) + +// Ask user password method +function askPassword(question) { + return new Promise((resolve, reject) => { + const rl = readLine.createInterface({ + input: process.stdin, + output: process.stdout + }) + + rl.question(question, answer => { + rl.close() + resolve(answer) + }) + }) +} + +// Generate hash password method +async function generateHash() { + try { + console.log('**********************************') + console.log('** Password hash script **') + console.log('**********************************') + + const passwordAnswer = await askPassword( + 'Please give me a password to hash: ' + ) + + if (passwordAnswer != '') { + const hashedPassword = hashPassword(passwordAnswer) + const compare = bcrypt.compareSync(passwordAnswer, hashedPassword) + await console.log('Hashed password:', hashedPassword) + await console.log('Valdiation:', compare) + } else { + console.log('You need write something. Script aborted!') + } + } catch (err) { + console.log(err) + return process.exit(1) + } +} + +generateHash() diff --git a/api_v1/users.json b/api_v1/users.json new file mode 100644 index 0000000..db83a6d --- /dev/null +++ b/api_v1/users.json @@ -0,0 +1,22 @@ +{ + "users": [ + { + "id": 1, + "name": "Admin", + "email": "admin@gmail.com", + "password": "$2b$08$g0UWN6RnN7e.8rX3fuXSSOSJDTvucu./0FAU.yXp0wx4SJXyeaU3." + }, + { + "id": 2, + "name": "Fabio", + "email": "fabio@gmail.com", + "password": "$2b$08$g0UWN6RnN7e.8rX3fuXSSOSJDTvucu./0FAU.yXp0wx4SJXyeaU3." + }, + { + "id": 3, + "name": "Jessica", + "email": "jessie@libero.it", + "password": "$2b$08$g0UWN6RnN7e.8rX3fuXSSOSJDTvucu./0FAU.yXp0wx4SJXyeaU3." + } + ] +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..50c62bf --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2570 @@ +{ + "name": "ssj", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ssj", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "async": "^3.2.6", + "axios": "^1.13.5", + "bcrypt": "^6.0.0", + "body-parser": "^2.2.2", + "dotenv": "^17.3.1", + "exifreader": "^4.36.2", + "json-server": "^0.17.4", + "jsonwebtoken": "^9.0.3", + "path": "^0.12.7", + "sharp": "^0.34.5" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", + "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==", + "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/@xmldom/xmldom": { + "version": "0.9.8", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.9.8.tgz", + "integrity": "sha512-p96FSY54r+WJ50FIOsCOjyj/wavs8921hG5+kVMmZgKcvIKxMXHTrjNJvRgWa/zuX3B6t2lijLNFaOyuxUH+2A==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14.6" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "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.5", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz", + "integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/basic-auth/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/bcrypt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz", + "integrity": "sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^8.3.0", + "node-gyp-build": "^4.8.4" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "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.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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/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/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "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/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "license": "MIT", + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", + "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "compressible": "~2.0.18", + "debug": "2.6.9", + "negotiator": "~0.6.4", + "on-headers": "~1.1.0", + "safe-buffer": "5.2.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/compression/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/compression/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/connect-pause": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/connect-pause/-/connect-pause-0.1.1.tgz", + "integrity": "sha512-a1gSWQBQD73krFXdUEYJom2RTFrWUL3YvXDCRkyv//GVXc79cdW9MngtRuN9ih4FDKBtfJAJId+BbDuX+1rh2w==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "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.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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/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/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "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/dotenv": { + "version": "17.3.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz", + "integrity": "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==", + "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/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "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/errorhandler": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/errorhandler/-/errorhandler-1.5.2.tgz", + "integrity": "sha512-kNAL7hESndBCrWwS72QyV3IVOTrVmj9D062FV5BQswNL5zEdeRmz/WJFyh6Aj/plvvSOrzddkxW57HgkZcR9Fw==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "escape-html": "~1.0.3" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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/exifreader": { + "version": "4.36.2", + "resolved": "https://registry.npmjs.org/exifreader/-/exifreader-4.36.2.tgz", + "integrity": "sha512-Rpboqge86aBhRVJeW70BZHIkFNCi6rVlidzKuDyRxsreS/SbT983wFk/88+ddJu7zSrOae5fuiyXO7X91qq88Q==", + "hasInstallScript": true, + "license": "MPL-2.0", + "optionalDependencies": { + "@xmldom/xmldom": "^0.9.4" + } + }, + "node_modules/express": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.14.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-urlrewrite": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/express-urlrewrite/-/express-urlrewrite-1.4.0.tgz", + "integrity": "sha512-PI5h8JuzoweS26vFizwQl6UTF25CAHSggNv0J25Dn/IKZscJHWZzPrI5z2Y2jgOzIaw2qh8l6+/jUcig23Z2SA==", + "license": "MIT", + "dependencies": { + "debug": "*", + "path-to-regexp": "^1.0.3" + } + }, + "node_modules/express-urlrewrite/node_modules/path-to-regexp": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.9.0.tgz", + "integrity": "sha512-xIp7/apCFJuUHdDLWe8O1HIkb0kQrOMb/0u6FXQjemHn/ii5LrIzU6bdECnsiTF/GjZkMEKg1xdiZwNqDYlZ6g==", + "license": "MIT", + "dependencies": { + "isarray": "0.0.1" + } + }, + "node_modules/express/node_modules/body-parser": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/express/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/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/express/node_modules/qs": { + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", + "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/express/node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/express/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/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "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/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": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "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-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "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/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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/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/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "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/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-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-promise": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", + "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==", + "license": "MIT" + }, + "node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", + "license": "MIT" + }, + "node_modules/jju": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/jju/-/jju-1.4.0.tgz", + "integrity": "sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==", + "license": "MIT" + }, + "node_modules/json-parse-helpfulerror": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/json-parse-helpfulerror/-/json-parse-helpfulerror-1.0.3.tgz", + "integrity": "sha512-XgP0FGR77+QhUxjXkwOMkC94k3WtqEBfcnjWqhRd82qTat4SWKRE+9kUnynz/shm3I4ea2+qISvTIeGTNU7kJg==", + "license": "MIT", + "dependencies": { + "jju": "^1.1.0" + } + }, + "node_modules/json-server": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/json-server/-/json-server-0.17.4.tgz", + "integrity": "sha512-bGBb0WtFuAKbgI7JV3A864irWnMZSvBYRJbohaOuatHwKSRFUfqtQlrYMrB6WbalXy/cJabyjlb7JkHli6dYjQ==", + "license": "MIT", + "dependencies": { + "body-parser": "^1.19.0", + "chalk": "^4.1.2", + "compression": "^1.7.4", + "connect-pause": "^0.1.1", + "cors": "^2.8.5", + "errorhandler": "^1.5.1", + "express": "^4.17.1", + "express-urlrewrite": "^1.4.0", + "json-parse-helpfulerror": "^1.0.3", + "lodash": "^4.17.21", + "lodash-id": "^0.14.1", + "lowdb": "^1.0.0", + "method-override": "^3.0.0", + "morgan": "^1.10.0", + "nanoid": "^3.1.23", + "please-upgrade-node": "^3.2.0", + "pluralize": "^8.0.0", + "server-destroy": "^1.0.1", + "yargs": "^17.0.1" + }, + "bin": { + "json-server": "lib/cli/bin.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/json-server/node_modules/body-parser": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/json-server/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/json-server/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/json-server/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/json-server/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/json-server/node_modules/qs": { + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", + "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/json-server/node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/json-server/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/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/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "license": "MIT" + }, + "node_modules/lodash-id": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/lodash-id/-/lodash-id-0.14.1.tgz", + "integrity": "sha512-ikQPBTiq/d5m6dfKQlFdIXFzvThPi2Be9/AHxktOnDSfSxE1j9ICbBT5Elk1ke7HSTgM38LHTpmJovo9/klnLg==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "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/lowdb": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lowdb/-/lowdb-1.0.0.tgz", + "integrity": "sha512-2+x8esE/Wb9SQ1F9IHaYWfsC9FIecLOPrK4g17FGEayjUWH172H6nwicRovGvSE2CPZouc2MCIqCI7h9d+GftQ==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.3", + "is-promise": "^2.1.0", + "lodash": "4", + "pify": "^3.0.0", + "steno": "^0.4.1" + }, + "engines": { + "node": ">=4" + } + }, + "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/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/method-override": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/method-override/-/method-override-3.0.0.tgz", + "integrity": "sha512-IJ2NNN/mSl9w3kzWB92rcdHpz+HjkxhDJWNDBqSlas+zQdP8wBiJzITPg08M/k2uVvMow7Sk41atndNtt/PHSA==", + "license": "MIT", + "dependencies": { + "debug": "3.1.0", + "methods": "~1.1.2", + "parseurl": "~1.3.2", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/method-override/node_modules/debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/method-override/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "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": "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/mime-types/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/morgan": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.1.tgz", + "integrity": "sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A==", + "license": "MIT", + "dependencies": { + "basic-auth": "~2.0.1", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-finished": "~2.3.0", + "on-headers": "~1.1.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/morgan/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/morgan/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/morgan/node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "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/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-addon-api": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz", + "integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==", + "license": "MIT", + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "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/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "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": { + "version": "0.12.7", + "resolved": "https://registry.npmjs.org/path/-/path-0.12.7.tgz", + "integrity": "sha512-aXXC6s+1w7otVF9UletFkFcDsJeO7lSZBPUQhtb5O0xJe8LtYhj/GxldoL09bBj9+ZmE2hNoHqQSFMN5fikh4Q==", + "license": "MIT", + "dependencies": { + "process": "^0.11.1", + "util": "^0.10.3" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/please-upgrade-node": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz", + "integrity": "sha512-gQR3WpIgNIKwBMVLkpMUeR3e1/E1y42bqDQZfql+kDeXd8COYfM8PQA4X6y7a8u9Ua9FHmsrrmirW2vHs45hWg==", + "license": "MIT", + "dependencies": { + "semver-compare": "^1.0.0" + } + }, + "node_modules/pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "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", + "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/qs": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", + "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/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", + "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", + "license": "MIT" + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/server-destroy": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/server-destroy/-/server-destroy-1.0.1.tgz", + "integrity": "sha512-rb+9B5YBIEzYcD6x2VKidaa+cqYBJQKnU4oe4E3ANwRRN56yk/ua1YCJT1n21NTS8w6CcOclAKNP3PhdCXKYtQ==", + "license": "ISC" + }, + "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/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/steno": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/steno/-/steno-0.4.4.tgz", + "integrity": "sha512-EEHMVYHNXFHfGtgjNITnka0aHhiAlo93F7z2/Pwd+g0teG9CnM3JIINM7hVVB5/rhw9voufD7Wukwgtw2uqh6w==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.3" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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/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/type-is/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/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": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz", + "integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==", + "license": "MIT", + "dependencies": { + "inherits": "2.0.3" + } + }, + "node_modules/util/node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", + "license": "ISC" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "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/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..dfea9ce --- /dev/null +++ b/package.json @@ -0,0 +1,28 @@ +{ + "name": "gallery-jwt-json-server", + "version": "0.0.1", + "description": "Gallery and JWT Protected REST API with json-server", + "main": "index.js", + "scripts": { + "start-no-auth": "json-server --watch ./api_v1/db.json -s ./public", + "start": "node ./api_v1/server.js -s ./public", + "hash": "node ./api_v1/tools.js" + }, + "keywords": [ + "api" + ], + "author": "Fabio", + "license": "MIT", + "dependencies": { + "async": "^3.2.6", + "axios": "^1.13.5", + "bcrypt": "^6.0.0", + "body-parser": "^2.2.2", + "dotenv": "^17.3.1", + "exifreader": "^4.36.2", + "json-server": "^0.17.4", + "jsonwebtoken": "^9.0.3", + "path": "^0.12.7", + "sharp": "^0.34.5" + } +} diff --git a/public/admin.html b/public/admin.html new file mode 100644 index 0000000..fa82788 --- /dev/null +++ b/public/admin.html @@ -0,0 +1,65 @@ + + + + + Photo Manager + + + +
+

Gestione Foto

+ + + + + + +

+
+ + + + + diff --git a/public/css/base.css b/public/css/base.css new file mode 100644 index 0000000..1111f49 --- /dev/null +++ b/public/css/base.css @@ -0,0 +1,27 @@ +:root { --header-h: 60px; } + +/* Safe-area iOS */ +@supports (top: env(safe-area-inset-top)) { + :root { --safe-top: env(safe-area-inset-top); } +} +@supports not (top: env(safe-area-inset-top)) { + :root { --safe-top: 0px; } +} + +body { + font-family: sans-serif; + margin: 0; + padding: 0; + background: #fafafa; +} + +/* Scrollbar */ +::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +::-webkit-scrollbar-thumb { + background: #ccc; + border-radius: 4px; +} diff --git a/public/css/bottomSheet.css b/public/css/bottomSheet.css new file mode 100644 index 0000000..0424e26 --- /dev/null +++ b/public/css/bottomSheet.css @@ -0,0 +1,200 @@ +/* ========================================= + Variabili globali + ========================================= */ +:root { + --header-height: 60px; /* cambia se il tuo header è più alto/basso */ +} + +/* ========================================= + MAPPA GLOBALE (contenitore sotto l’header) + ========================================= */ +.global-map { + position: fixed; + top: var(--header-height); + left: 0; + right: 0; + bottom: 0; + + width: 100%; + display: none; /* visibile solo con .open */ + z-index: 10; /* sotto a bottom-sheet (9999) e modal (10000) */ + background: #000; /* evita flash bianco durante init */ +} + +.global-map.open { + display: block; +} + +/* Leaflet riempie il contenitore */ +.global-map .leaflet-container { + width: 100%; + height: 100%; +} + +/* Marker immagine (miniatura) */ +.leaflet-marker-icon.photo-marker { + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0,0,0,0.25); + border: 2px solid rgba(255,255,255,0.9); + background: #fff; +} + +/* Nascondi la gallery quando la mappa è aperta */ +.gallery.hidden { + display: none !important; +} + +/* ========================================= + BOTTOM SHEET — struttura base comune + ========================================= */ +.bottom-sheet { + position: fixed; + bottom: 0; + left: 0; + width: 100%; + + background: rgba(255,255,255,0.95); + backdrop-filter: blur(6px); + border-top: 1px solid #ddd; + box-shadow: 0 -2px 10px rgba(0,0,0,0.15); + + display: none; /* diventa flex con .open */ + flex-direction: column; + z-index: 9999; /* molto alto: il modal starà sopra (10000) */ +} + +.bottom-sheet.open { + display: flex; +} + +/* Maniglia superiore */ +.sheet-header { + height: 16px; + display: flex; + justify-content: center; + align-items: center; +} + +.sheet-header::before { + content: ""; + width: 40px; + height: 4px; + background: #bbb; + border-radius: 4px; +} + +/* ========================================= + BOTTOM SHEET FOTO (strip bassa come nel vecchio) + ========================================= */ +.photo-strip { + height: 140px; /* altezza originale della strip */ + overflow-y: hidden; /* niente scroll verticale */ + overflow-x: auto; /* scroll orizzontale per le foto */ +} + +/* Contenitore elementi della strip — compatibile con id e class */ +#sheetGallery, +.sheet-gallery { + display: flex; + flex-direction: row; + overflow-x: auto; + padding: 10px; + gap: 10px; + + -webkit-overflow-scrolling: touch; + scroll-snap-type: x proximity; +} + +/* Singolo elemento della strip */ +.sheet-item { + width: 90px; + height: 90px; + border-radius: 10px; + overflow: hidden; + flex-shrink: 0; + box-shadow: 0 2px 6px rgba(0,0,0,0.25); + background: #eee; + scroll-snap-align: start; +} + +/* Miniatura della foto nella strip */ +.sheet-thumb, +.sheet-item img { + width: 100%; + height: 100%; + object-fit: cover; + display: block; + border-radius: 8px; /* alias; la .sheet-item ha già 10px */ +} + +/* ========================================= + BOTTOM SHEET OPZIONI (⋮) — menu grande + ========================================= */ +.options-sheet { + height: auto; + max-height: 80vh; + overflow-y: auto; +} + +.sheet-content { + padding: 20px; +} + +.sheet-btn { + width: 100%; + padding: 12px; + margin-bottom: 8px; + text-align: left; + background: #f5f5f5; + border: none; + border-radius: 8px; + font-size: 15px; + cursor: pointer; +} + +.sheet-btn:hover { + background: #e8e8e8; +} + +#optionsSheet h3 { + margin-top: 20px; + margin-bottom: 10px; + font-size: 16px; + color: #444; +} + +/* ========================================= + OVERLAY per chiusura sheet/option + ========================================= */ +.sheet-overlay { + position: fixed; + inset: 0; + background: rgba(0,0,0,0.0); /* invisibile ma cliccabile */ + display: none; + z-index: 9998; /* appena sotto il bottom sheet */ +} + +.sheet-overlay.open { + display: block; +} + +/* ========================================= + MODAL sopra allo sheet + ========================================= */ +.modal.open { + z-index: 10000 !important; /* sopra al bottom sheet (9999) */ +} + +/* ========================================= + Piccoli affinamenti facoltativi + ========================================= */ +/* scrollbar sottile solo per la strip (opzionale) */ +#sheetGallery::-webkit-scrollbar, +.sheet-gallery::-webkit-scrollbar { + height: 8px; +} +#sheetGallery::-webkit-scrollbar-thumb, +.sheet-gallery::-webkit-scrollbar-thumb { + background: rgba(0,0,0,0.25); + border-radius: 4px; +} \ No newline at end of file diff --git a/public/css/gallery.css b/public/css/gallery.css new file mode 100644 index 0000000..ecb3e1d --- /dev/null +++ b/public/css/gallery.css @@ -0,0 +1,46 @@ +.gallery { + display: block; + padding: 6px; /* più stretto */ +} + +.gallery-section-title { + font-size: 18px; + font-weight: 600; + margin: 18px 6px 6px; /* più compatto */ + color: #444; +} + +.gallery-section { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); /* leggermente più piccole */ + gap: 6px; /* SPACING RIDOTTO */ + padding: 0 6px; +} + +.thumb { + width: 100%; + aspect-ratio: 1 / 1; + border-radius: 8px; /* più compatto */ + overflow: hidden; + background: white; + box-shadow: 0 1px 3px rgba(0,0,0,0.12); /* più leggero */ + position: relative; + cursor: pointer; +} + +.thumb img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.play-icon { + position: absolute; + bottom: 6px; + right: 6px; + background: rgba(0,0,0,0.55); + color: white; + padding: 3px 5px; + border-radius: 4px; + font-size: 12px; +} diff --git a/public/css/header.css b/public/css/header.css new file mode 100644 index 0000000..348c015 --- /dev/null +++ b/public/css/header.css @@ -0,0 +1,84 @@ +header { + padding: 10px 15px; + background: #333; + color: white; + display: flex; + justify-content: space-between; + align-items: center; + position: sticky; /* o fixed se preferisci header sempre fisso */ + top: 0; + z-index: 100; /* > mappa (che sta a 50) */ + +} + +.top-buttons { + display: flex; + gap: 10px; +} + +.icon-btn { + background: none; + border: none; + font-size: 22px; + padding: 6px 10px; + cursor: pointer; + border-radius: 6px; +} + +.icon-btn:hover { + background: rgba(255,255,255,0.15); +} + +.icon-btn { + background: none; + border: none; + font-size: 22px; + padding: 6px 10px; + cursor: pointer; + border-radius: 6px; + color: white; /* 🔥 questo mancava */ +} + +/* Testo solo per screen reader */ +.sr-only { + position: absolute !important; + width: 1px; height: 1px; + padding: 0; margin: -1px; + overflow: hidden; clip: rect(0,0,1px,1px); + white-space: nowrap; border: 0; +} + +/* Bottone */ +.icon-btn.logout-btn { + --size: 36px; + --bg-hover: rgba(255,255,255,0.12); /* hover chiaro su sfondo nero */ + --ring: rgba(255,255,255,0.35); /* focus ring chiaro */ + + width: var(--size); + height: var(--size); + padding: 0; + display: inline-flex; + align-items: center; + justify-content: center; + background: transparent; + border: none; + border-radius: 50%; + cursor: pointer; + transition: background-color .15s ease, box-shadow .15s ease, transform .05s ease; +} + +.icon-btn.logout-btn:hover { background: var(--bg-hover); } +.icon-btn.logout-btn:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--ring); } +.icon-btn.logout-btn:active { transform: translateY(1px); } + +/* PNG bianco: se l'originale è scuro/nero su trasparente */ +.logout-icon { + width: 22px; + height: 22px; + display: block; + filter: brightness(0) invert(1); /* → bianco */ + /* facoltativo, per nitidezza */ + image-rendering: -webkit-optimize-contrast; +} + +/* Se hai una versione retina 2x, usa srcset nell'HTML */ \ No newline at end of file diff --git a/public/css/infoPanel.css b/public/css/infoPanel.css new file mode 100644 index 0000000..77794fc --- /dev/null +++ b/public/css/infoPanel.css @@ -0,0 +1,44 @@ +.info-panel { + position: fixed; + top: 0; + right: 0; + width: 320px; + height: 100%; + background: #fff; + padding: 16px; + box-shadow: -2px 0 6px rgba(0,0,0,0.25); + overflow-y: auto; + z-index: 10000; + transform: translateX(100%); + transition: transform 0.3s ease; +} + +.info-panel.open { + transform: translateX(0); +} + +.info-panel h3 { + margin-top: 0; +} + +.info-row { + margin-bottom: 10px; +} + +.info-row b { + display: inline-block; + width: 110px; +} + +.info-map { + width: 100%; + height: 250px; + margin-top: 15px; + border-radius: 6px; + overflow: hidden; + border: 1px solid #ccc; +} + +.info-spacer { + height: 16px; /* o 20px se vuoi più spazio */ +} diff --git a/public/css/login.css b/public/css/login.css new file mode 100644 index 0000000..91f5dc8 --- /dev/null +++ b/public/css/login.css @@ -0,0 +1,27 @@ + +.login-modal { + position: fixed; + inset: 0; + background: rgba(0,0,0,0.7); + display: none; + align-items: center; + justify-content: center; + z-index: 20000; +} + +.login-box { + background: white; + padding: 20px; + border-radius: 12px; + width: 280px; + display: flex; + flex-direction: column; + gap: 12px; +} + +.login-error { + color: red; + font-size: 14px; + min-height: 18px; +} + diff --git a/public/css/map.css b/public/css/map.css new file mode 100644 index 0000000..3980083 --- /dev/null +++ b/public/css/map.css @@ -0,0 +1,88 @@ +/* =============================== + MAPPA GLOBALE + =============================== */ + +/* La mappa occupa tutto lo schermo SOTTO l’header */ +.global-map { + position: fixed; + left: 0; + right: 0; + top: calc(var(--header-h, 60px) + var(--safe-top, 0px)); /* niente hard-code */ + bottom: 0; + z-index: 50; + + display: none; /* chiusa di default */ +} + +/* Quando è aperta, visibile */ +.global-map.open { + display: block; +} + +/* La Leaflet container deve riempire il contenitore */ +.global-map, +.global-map .leaflet-container { + width: 100%; + height: 100%; +} + +/* Nasconde la gallery quando la mappa è aperta */ +.gallery.hidden { + display: none; +} + +/* =============================== + MARKER FOTO + =============================== */ + +.photo-marker { + width: 48px; + height: 48px; + border-radius: 10px; + overflow: hidden; + position: relative; + box-shadow: 0 2px 6px rgba(0,0,0,0.25); + background: #fff; +} + +.photo-marker img { + width: 100%; + height: 100%; + object-fit: cover; +} + +/* =============================== + CLUSTER + =============================== */ + +.photo-cluster { + width: 56px; + height: 56px; + position: relative; + border-radius: 12px; + overflow: visible; +} + +.cluster-back { + position: absolute; + top: 6px; + left: 6px; + width: 48px; + height: 48px; + border-radius: 10px; + object-fit: cover; + opacity: 0.5; + filter: blur(1px); + transform: scale(0.95); +} + +.cluster-front { + position: absolute; + top: 0; + left: 0; + width: 48px; + height: 48px; + border-radius: 10px; + object-fit: cover; + box-shadow: 0 2px 6px rgba(0,0,0,0.35); +} \ No newline at end of file diff --git a/public/css/modal.css b/public/css/modal.css new file mode 100644 index 0000000..f3da73e --- /dev/null +++ b/public/css/modal.css @@ -0,0 +1,299 @@ +/* =============================== + MODAL OVERLAY + =============================== */ + +.modal { + position: fixed; + inset: 0; /* top:0 right:0 bottom:0 left:0 */ + background: rgba(0,0,0,0.8); + display: none; /* chiuso di default */ + align-items: center; + justify-content: center; + z-index: 9999; /* sopra a qualunque overlay/sheet */ + overflow: hidden; /* evita scroll sullo sfondo */ + /* Animazione di fade */ + opacity: 0; + transition: opacity 160ms ease-out; +} + +.modal.open { + display: flex; + opacity: 1; +} + +/* effetto vetro opzionale dove supportato */ +@supports (backdrop-filter: blur(4px)) { + .modal { + backdrop-filter: blur(4px); + } +} + +/* =============================== + CONTENITORE CONTENUTI + =============================== */ + +.modal-content { + width: 90vw; + height: 90vh; + max-width: 1200px; + max-height: 90vh; + position: relative; + display: flex; + justify-content: center; + align-items: center; + + /* Animazione di scale-in */ + transform: scale(0.98); + transition: transform 160ms ease-out; +} + +.modal.open .modal-content { + transform: scale(1); +} + +/* Ridimensionamento su mobile */ +@media (max-width: 768px) { + .modal-content { + width: 100vw; + height: 100vh; + } +} + +/* Contenitore del media */ +#modalMediaContainer { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + + /* Evita che clic sul media “passino” al layer sotto */ + position: relative; + z-index: 1; +} + +/* Immagini e video si adattano all’area */ +#modalMediaContainer img, +#modalMediaContainer video { + width: 100%; + height: 100%; + object-fit: contain; + display: block; + background: #000; /* evita flash bianco */ + position: relative; /* crea contesto */ + z-index: 1; /* sotto ai pulsanti */ +} + +/* =============================== + PULSANTE CHIUSURA (X) + =============================== */ + +/* FISSO sopra al video, con safe-area per iPhone */ +.modal-close { + position: fixed; /* <-- chiave: resta sopra al video anche con stacking strani */ + top: calc(8px + env(safe-area-inset-top)); + right: calc(12px + env(safe-area-inset-right)); + z-index: 10001; /* il modal è 9999 */ + + background: rgba(0,0,0,0.35); + color: #fff; + border-radius: 22px; + min-width: 44px; /* target minimo consigliato */ + height: 44px; + padding: 0 10px; + + display: inline-flex; + align-items: center; + justify-content: center; + + cursor: pointer; + font-weight: 700; + border: 1px solid rgba(255,255,255,0.25); + box-shadow: 0 2px 8px rgba(0,0,0,0.4); + user-select: none; + line-height: 1; +} + +/* area di hit più ampia senza cambiare il look */ +.modal-close::after { + content: ""; + position: absolute; + inset: -8px; /* allarga di 8px tutt’intorno */ +} + +.modal-close:hover { + background: rgba(0,0,0,0.5); +} + +.modal-close:active { + transform: translateY(1px); +} + +.modal-close:focus-visible { + outline: 2px solid #4c9ffe; + outline-offset: 2px; + border-radius: 8px; +} + +/* =============================== + PULSANTE INFO (ℹ️) + =============================== */ + +.modal-info-btn { + position: absolute; + bottom: 12px; + right: 16px; + + background: #fff; + border-radius: 50%; + width: 40px; + height: 40px; + + display: inline-flex; + align-items: center; + justify-content: center; + + cursor: pointer; + border: 1px solid #d0d0d0; + font-size: 20px; + z-index: 10000; /* sopra al media, sotto alla X */ + box-shadow: 0 2px 8px rgba(0,0,0,0.3); + + /* 🔒 Disattiva selezione e popup dizionario */ + user-select: none; + -webkit-user-select: none; + -webkit-touch-callout: none; + -webkit-tap-highlight-color: transparent; +} + +.modal-info-btn:hover { + background: #f7f7f7; +} + +.modal-info-btn:active { + transform: scale(0.95); +} + +.modal-info-btn:focus-visible { + outline: 2px solid #4c9ffe; + outline-offset: 2px; +} + + +/* ℹ️ evidenziato quando il pannello info è aperto */ +.modal-info-btn.active { + background: #f7f7f7; + border-color: #cfcfcf; + box-shadow: 0 2px 8px rgba(0,0,0,0.25); + transform: none; +} + +/* =============================== + (OPZIONALE) LINK "APRI ORIGINALE ↗" + =============================== */ + +.modal-open-original { + position: absolute; + top: 8px; + right: 56px; /* lascia spazio alla X */ + background: rgba(255,255,255,0.95); + color: #000; + border-radius: 16px; + height: 32px; + padding: 0 10px; + display: inline-flex; + align-items: center; + gap: 6px; + cursor: pointer; + border: 1px solid #d0d0d0; + font-size: 13px; + z-index: 10000; /* sopra al media */ + text-decoration: none; + box-shadow: 0 2px 8px rgba(0,0,0,0.3); +} + +.modal-open-original:hover { + background: #fff; +} + +.modal-open-original:focus-visible { + outline: 2px solid #4c9ffe; + outline-offset: 2px; + border-radius: 6px; +} + +/* =============================== + MODAL STATE UTILI + =============================== */ + +body.no-scroll { + overflow: hidden; +} + +/* High contrast / accessibility (opzionale) */ +@media (prefers-contrast: more) { + .modal { + background: rgba(0,0,0,0.9); + } + .modal-close, + .modal-info-btn, + .modal-open-original { + border-color: #000; + box-shadow: none; + } +} + +/* Riduci animazioni se l’utente lo preferisce */ +@media (prefers-reduced-motion: reduce) { + .modal, + .modal-content { + transition: none !important; + } +} + + + +/* =============================== + FRECCE DI NAVIGAZIONE < > + =============================== */ + +.modal-nav-btn { + position: fixed; /* fisso: resta sopra a video/immagine */ + top: calc(50% + env(safe-area-inset-top)); + transform: translateY(-50%); + z-index: 10000; /* sopra al media, sotto alla X (10001) */ + + width: 44px; + height: 44px; + border-radius: 22px; + border: 1px solid rgba(255,255,255,0.25); + background: rgba(0,0,0,0.35); + color: #fff; + font-size: 22px; + line-height: 1; + display: inline-flex; + align-items: center; + justify-content: center; + cursor: pointer; + + user-select: none; + -webkit-user-select: none; + -webkit-tap-highlight-color: transparent; + box-shadow: 0 2px 8px rgba(0,0,0,0.4); + + transition: background-color .15s ease, transform .05s ease; +} + +.modal-nav-btn:hover { background: rgba(0,0,0,0.5); } +.modal-nav-btn:active { transform: translateY(-50%) translateY(1px); } +.modal-nav-btn:focus-visible { + outline: 2px solid #4c9ffe; + outline-offset: 2px; + border-radius: 8px; +} + +.modal-nav-btn.prev { left: calc(12px + env(safe-area-inset-left)); } +.modal-nav-btn.next { right: calc(12px + env(safe-area-inset-right)); } + +/* Nascondi automaticamente se c'è un solo elemento */ +.modal-nav-btn.hidden { display: none !important; } diff --git a/public/css/optionsSheet.css b/public/css/optionsSheet.css new file mode 100644 index 0000000..378d064 --- /dev/null +++ b/public/css/optionsSheet.css @@ -0,0 +1,26 @@ +#optionsSheet .sheet-content { + padding: 20px; +} + +#optionsSheet h3 { + margin-top: 20px; + margin-bottom: 10px; + font-size: 16px; + color: #444; +} + +.sheet-btn { + width: 100%; + padding: 12px; + margin-bottom: 8px; + text-align: left; + background: #f5f5f5; + border: none; + border-radius: 8px; + font-size: 15px; + cursor: pointer; +} + +.sheet-btn:hover { + background: #e8e8e8; +} diff --git a/public/css/utils.css b/public/css/utils.css new file mode 100644 index 0000000..d88c06f --- /dev/null +++ b/public/css/utils.css @@ -0,0 +1,24 @@ +.hidden { + display: none !important; +} + +.rounded { + border-radius: 10px; +} + +.shadow-sm { + box-shadow: 0 1px 3px rgba(0,0,0,0.15); +} + +.shadow-md { + box-shadow: 0 2px 6px rgba(0,0,0,0.25); +} + +.fade-in { + animation: fadeIn 0.3s ease; +} + +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} diff --git a/public/img/switch.png b/public/img/switch.png new file mode 100644 index 0000000..bede7f7 Binary files /dev/null and b/public/img/switch.png differ diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..6226516 --- /dev/null +++ b/public/index.html @@ -0,0 +1,177 @@ + + + + + Galleria Foto + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

Galleria Foto

+ +
+ + + + +
+
+ +
+ + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + +
+
+ +
+ + + + +
+
+ +
+ +

Ordinamento

+ + + +

Raggruppamento

+ + + + + +

Filtri

+ + + + +
+
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/js/bottomSheet.js b/public/js/bottomSheet.js new file mode 100644 index 0000000..9c52ed1 --- /dev/null +++ b/public/js/bottomSheet.js @@ -0,0 +1,121 @@ +// =============================== +// BOTTOM SHEET — strip multi-foto (2+); singola → modal diretto +// Regola: aprendo qualcosa di nuovo, chiudo il precedente +// =============================== + +const bottomSheet = document.getElementById("bottomSheet"); +const sheetGallery = document.getElementById("sheetGallery"); +let optionsSheetRef = document.getElementById("optionsSheet"); + +// Overlay (creazione difensiva) +let sheetOverlay = document.getElementById("sheetOverlay"); +if (!sheetOverlay) { + sheetOverlay = document.createElement("div"); + sheetOverlay.id = "sheetOverlay"; + sheetOverlay.className = "sheet-overlay"; + document.body.appendChild(sheetOverlay); +} + +function openBottomSheet(photoList) { + const list = Array.isArray(photoList) ? photoList : []; + + // 0 o 1 foto → MODAL diretto, nessuna bottom-zone + if (list.length <= 1) { + const p = list[0]; + if (p) { + const thumbUrl = (typeof toAbsoluteUrl === "function") + ? toAbsoluteUrl(p.thub2 || p.thub1 || p.path) + : (p.thub2 || p.thub1 || p.path); + + const originalUrl = (typeof toAbsoluteUrl === "function") + ? toAbsoluteUrl(p.path) + : p.path; + + closeBottomSheet(); + + if (typeof window.openModalFromList === "function") { + window.openModalFromList([p], 0); + } else { + window.openModal?.(originalUrl, thumbUrl, p); + } + } + return; + } + + // 2+ foto → strip in basso + sheetGallery.innerHTML = ""; + + list.forEach((photo, index) => { + const thumbUrl = (typeof toAbsoluteUrl === "function") + ? toAbsoluteUrl(photo.thub2 || photo.thub1) + : (photo.thub2 || photo.thub1); + + const originalUrl = (typeof toAbsoluteUrl === "function") + ? toAbsoluteUrl(photo.path) + : photo.path; + + const div = document.createElement("div"); + div.className = "sheet-item"; + div.tabIndex = 0; + + const img = document.createElement("img"); + img.className = "sheet-thumb"; + img.src = thumbUrl; + img.alt = photo?.name || ""; + img.loading = "lazy"; + + const openFromIndex = () => { + closeBottomSheet(); + if (typeof window.openModalFromList === "function") { + window.openModalFromList(list, index); + } else { + window.openModal?.(originalUrl, thumbUrl, photo); + } + }; + + div.addEventListener("click", openFromIndex); + div.addEventListener("keydown", (e) => { + if (e.key === "Enter" || e.key === " ") { e.preventDefault(); openFromIndex(); } + }); + + div.appendChild(img); + sheetGallery.appendChild(div); + }); + + bottomSheet.classList.add("open"); + sheetOverlay.classList.add("open"); +} + +function closeBottomSheet() { + bottomSheet.classList.remove("open"); + sheetOverlay.classList.remove("open"); +} + +function openOptionsSheet() { + optionsSheetRef?.classList.add("open"); + sheetOverlay.classList.add("open"); +} +function closeOptionsSheet() { + optionsSheetRef?.classList.remove("open"); + sheetOverlay.classList.remove("open"); +} + +// Chiusura cliccando fuori +sheetOverlay.addEventListener("click", () => { + closeBottomSheet(); + closeOptionsSheet(); +}); + +// Chiusura toccando la maniglia +document.querySelectorAll(".sheet-header").forEach(header => { + header.addEventListener("click", () => { + closeBottomSheet(); + closeOptionsSheet(); + }); +}); + +// Export +window.openBottomSheet = openBottomSheet; +window.closeBottomSheet = closeBottomSheet; +window.openOptionsSheet = openOptionsSheet; +window.closeOptionsSheet = closeOptionsSheet; \ No newline at end of file diff --git a/public/js/config.js b/public/js/config.js new file mode 100644 index 0000000..9d429a4 --- /dev/null +++ b/public/js/config.js @@ -0,0 +1,45 @@ +// =============================== +// CONFIG DINAMICA DAL SERVER +// =============================== + +window.BASE_URL = null; +window.PHOTOS_URL = null; +window.MEDIA_BASE_ORIGIN = null; +window.configReady = false; + +// Carica /config dal backend + +(async () => { + try { + const res = await fetch('/config'); + const cfg = await res.json(); + + window.BASE_URL = cfg.baseUrl; + window.PHOTOS_URL = `${window.BASE_URL}/photos`; + window.MEDIA_BASE_ORIGIN = new URL(window.PHOTOS_URL).origin; + + console.log("[config] BASE_URL:", window.BASE_URL); + console.log("[config] PHOTOS_URL:", window.PHOTOS_URL); + console.log("[config] MEDIA_BASE_ORIGIN:", window.MEDIA_BASE_ORIGIN); + + window.configReady = true; + + } catch (err) { + console.error("[config] Errore nel caricamento della config:", err); + } +})(); + + + +// =============================== +// Utility: normalizza URL dei media +// =============================== +function toAbsoluteUrl(pathOrUrl) { + if (!pathOrUrl) return ''; + if (/^https?:\/\//i.test(pathOrUrl)) return pathOrUrl; + + const normalized = pathOrUrl.startsWith('/') ? pathOrUrl : `/${pathOrUrl}`; + return `${window.MEDIA_BASE_ORIGIN}${normalized}`; +} + +window.toAbsoluteUrl = toAbsoluteUrl; diff --git a/public/js/data.js b/public/js/data.js new file mode 100644 index 0000000..fc482f6 --- /dev/null +++ b/public/js/data.js @@ -0,0 +1,84 @@ +// =============================== +// FETCH DELLE FOTO +// =============================== +async function loadPhotos() { + console.log("Inizio fetch:", window.PHOTOS_URL); + + let res; + try { + res = await fetch(window.PHOTOS_URL, { + headers: { + "Authorization": "Bearer " + window.token + } + }); + } catch (e) { + console.error("Errore fetch:", e); + return; + } + + const text = await res.text(); + + if (!res.ok) { + console.error(`HTTP ${res.status} ${res.statusText} – body:`, text.slice(0, 200)); + return; + } + + try { + const parsed = JSON.parse(text); + + if (!Array.isArray(parsed)) { + console.error("La risposta non è un array:", parsed); + return; + } + + window.photosData = parsed; + console.log("JSON parse OK, numero foto:", parsed.length); + + } catch (e) { + console.error("Errore nel parse JSON:", e); + return; + } + + refreshGallery(); +} + +async function loadPhotos1() { + console.log("Inizio fetch:", window.BASE_URL + "/photos"); + + let res; + try { + res = await fetch(`${window.BASE_URL}/photos`, { + headers: { + "Authorization": "Bearer " + window.token + } + }); + } catch (e) { + console.error("Errore fetch:", e); + return; + } + + const text = await res.text(); + + if (!res.ok) { + console.error(`HTTP ${res.status} ${res.statusText} – body:`, text.slice(0, 200)); + return; + } + + try { + const parsed = JSON.parse(text); + + if (!Array.isArray(parsed)) { + console.error("La risposta non è un array:", parsed); + return; + } + + window.photosData = parsed; + console.log("JSON parse OK, numero foto:", parsed.length); + + } catch (e) { + console.error("Errore nel parse JSON:", e); + return; + } + + refreshGallery(); +} diff --git a/public/js/gallery.js b/public/js/gallery.js new file mode 100644 index 0000000..80b80bc --- /dev/null +++ b/public/js/gallery.js @@ -0,0 +1,139 @@ +// =============================== +// GALLERY — completa, stile Google Photos +// - Ordinamento +// - Filtri +// - Raggruppamento (auto/giorno/mese/anno) +// - Render a sezioni +// - Click: openModalFromList(sezione, indice) se disponibile (fallback openModal) +// =============================== + +// ORDINAMENTO +function sortByDate(photos, direction = "desc") { + return photos.slice().sort((a, b) => { + const da = a?.taken_at ? new Date(a.taken_at) : 0; + const db = b?.taken_at ? new Date(b.taken_at) : 0; + return direction === "asc" ? (da - db) : (db - da); + }); +} + +// FILTRI +function applyFilters(photos) { + if (!window.currentFilter) return photos; + + switch (window.currentFilter) { + case "folder": + return photos.filter(p => p.folder || (p.path && p.path.includes('/photos/'))); + case "location": + return photos.filter(p => p?.gps && p.gps.lat); + case "type": + return photos.filter(p => p?.mime_type && p.mime_type.startsWith("image/")); + default: + return photos; + } +} + +// RAGGRUPPAMENTO STILE GOOGLE PHOTOS +function groupByDate(photos, mode = "auto") { + const sections = []; + const now = new Date(); + + function getLabel(photo) { + const date = photo?.taken_at ? new Date(photo.taken_at) : null; + if (!date || isNaN(+date)) return "Senza data"; + + const diffDays = Math.floor((now - date) / (1000 * 60 * 60 * 24)); + if (mode === "day") return formatDay(date); + if (mode === "month") return formatMonth(date); + if (mode === "year") return date.getFullYear().toString(); + + if (diffDays === 0) return "Oggi"; + if (diffDays === 1) return "Ieri"; + if (diffDays <= 7) return "Questa settimana"; + if (diffDays <= 14) return "La settimana scorsa"; + if (diffDays <= 30) return "Questo mese"; + if (diffDays <= 60) return "Mese scorso"; + + if (date.getFullYear() === now.getFullYear()) return formatMonth(date); + return date.getFullYear().toString(); + } + + photos.forEach(photo => { + const label = getLabel(photo); + let section = sections.find(s => s.label === label); + if (!section) { + section = { label, photos: [] }; + sections.push(section); + } + section.photos.push(photo); + }); + + return sections; +} + +// FORMATTATORI +function formatDay(date) { + return date.toLocaleDateString("it-IT", { weekday: "long", day: "numeric", month: "long" }); +} +function formatMonth(date) { + return date.toLocaleDateString("it-IT", { month: "long", year: "numeric" }); +} + +// RENDER +function renderGallery(sections) { + const gallery = document.getElementById("gallery"); + if (!gallery) return; + gallery.innerHTML = ""; + + sections.forEach(section => { + const h = document.createElement("h2"); + h.className = "gallery-section-title"; + h.textContent = section.label; + gallery.appendChild(h); + + const container = document.createElement("div"); + container.className = "gallery-section"; + + section.photos.forEach((photo, idx) => { + const thumbDiv = document.createElement("div"); + thumbDiv.className = "thumb"; + + const th1 = (typeof toAbsoluteUrl === 'function') ? toAbsoluteUrl(photo?.thub1) : photo?.thub1; + const th2 = (typeof toAbsoluteUrl === 'function') ? toAbsoluteUrl(photo?.thub2 || photo?.thub1) : (photo?.thub2 || photo?.thub1); + const original = (typeof toAbsoluteUrl === 'function') ? toAbsoluteUrl(photo?.path) : photo?.path; + + const img = document.createElement("img"); + img.src = th1 || th2 || original || ""; + img.alt = photo?.name || ""; + img.loading = "lazy"; + thumbDiv.appendChild(img); + + if (photo?.mime_type && photo.mime_type.startsWith("video/")) { + const play = document.createElement("div"); + play.className = "play-icon"; + play.textContent = "▶"; + thumbDiv.appendChild(play); + } + + thumbDiv.addEventListener("click", () => { + // Chiudi sempre la strip prima di aprire una nuova foto + window.closeBottomSheet?.(); + + if (typeof window.openModalFromList === "function") { + window.openModalFromList(section.photos, idx); + } else { + window.openModal?.(original, th2, photo); + } + }); + + container.appendChild(thumbDiv); + }); + + gallery.appendChild(container); + }); +} + +// Esporti su window +window.sortByDate = sortByDate; +window.applyFilters = applyFilters; +window.groupByDate = groupByDate; +window.renderGallery = renderGallery; \ No newline at end of file diff --git a/public/js/infoPanel.js b/public/js/infoPanel.js new file mode 100644 index 0000000..11ca303 --- /dev/null +++ b/public/js/infoPanel.js @@ -0,0 +1,167 @@ +// =============================== +// PANNELLO INFO + MAPPA (toggle affidabile + auto-refresh su cambio foto) +// =============================== + +const infoPanel = document.getElementById('infoPanel'); +let infoMapInstance = null; // tieni traccia della mappa per pulizia + +// ------------------------------- +// Helpers UI / stato +// ------------------------------- +function isPanelOpen() { + return infoPanel.classList.contains('open') || + infoPanel.getAttribute('aria-hidden') === 'false' || + infoPanel.getAttribute('data-open') === '1' || + infoPanel.style.display === 'block'; +} + +function markButtonActive(active) { + const btn = document.getElementById('modalInfoBtn'); + if (btn) btn.classList.toggle('active', !!active); +} + +// ------------------------------- +// Render contenuti + (ri)creazione mappa +// ------------------------------- +function renderInfo(photo) { + if (!photo) return; + + const gps = photo.gps || { lat: '-', lng: '-', alt: '-' }; + const folder = photo.path?.split('/').slice(2, -1).join('/') || '-'; + const loc = photo.location || {}; + + // Inietta contenuti + infoPanel.innerHTML = ` +

Informazioni

+ +
Nome: ${photo.name ?? '-'}
+
Data: ${photo.taken_at ?? '-'}
+ +
Latitudine: ${gps.lat ?? '-'}
+
Longitudine: ${gps.lng ?? '-'}
+
Altitudine: ${gps.alt ?? '-'} m
+ +
Dimensioni: ${photo.width ?? '-'} × ${photo.height ?? '-'}
+
Peso: ${photo.size_bytes ? (photo.size_bytes / 1024 / 1024).toFixed(2) + ' MB' : '-'}
+
Tipo: ${photo.mime_type ?? '-'}
+ +
Cartella: ${folder}
+ +
+ +

Mappa

+ ${gps.lat !== '-' && gps.lng !== '-' ? '
' : ''} + +
+ +

Location

+ ${loc.continent ? `
Continente: ${loc.continent}
` : ''} + ${loc.country ? `
Nazione: ${loc.country}
` : ''} + ${loc.region ? `
Regione: ${loc.region}
` : ''} + ${loc.city ? `
Città: ${loc.city}
` : ''} + ${loc.address ? `
Indirizzo: ${loc.address}
` : ''} + ${loc.postcode ? `
CAP: ${loc.postcode}
` : ''} + ${loc.county_code ? `
Provincia: ${loc.county_code}
` : ''} + ${loc.timezone ? `
Timezone: ${loc.timezone}
` : ''} + ${loc.time ? `
Offset: ${loc.time}
` : ''} + `; + + // (Ri)crea la mappa se ci sono coordinate + // 1) Pulisci istanza precedente (evita "Map container is already initialized") + try { infoMapInstance?.remove(); } catch {} + infoMapInstance = null; + + if (gps.lat !== '-' && gps.lng !== '-') { + setTimeout(() => { + try { + infoMapInstance = L.map('infoMap', { + zoomControl: false, + attributionControl: false + }).setView([gps.lat, gps.lng], 13); + + L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { + maxZoom: 19 + }).addTo(infoMapInstance); + + L.marker([gps.lat, gps.lng]).addTo(infoMapInstance); + } catch (err) { + console.warn('Errore creazione mappa info:', err); + } + }, 50); + } +} + +// ------------------------------- +// API pubbliche: apri / chiudi / toggle +// ------------------------------- +window.openInfoPanel = function openInfoPanel(photo) { + renderInfo(photo || window.currentPhoto); + infoPanel.classList.add('open'); + infoPanel.setAttribute('aria-hidden', 'false'); + infoPanel.setAttribute('data-open', '1'); + markButtonActive(true); +}; + +window.closeInfoPanel = function closeInfoPanel() { + infoPanel.classList.remove('open'); + infoPanel.setAttribute('aria-hidden', 'true'); + infoPanel.setAttribute('data-open', '0'); + markButtonActive(false); + + // pulizia mappa + try { infoMapInstance?.remove(); } catch {} + infoMapInstance = null; +}; + +window.toggleInfoPanel = function toggleInfoPanel(photo) { + if (isPanelOpen()) window.closeInfoPanel(); + else window.openInfoPanel(photo || window.currentPhoto); +}; + +// ------------------------------- +// Delegation: click su ℹ️ = TOGGLE vero +// ------------------------------- +document.addEventListener('click', (e) => { + if (e.target.id !== 'modalInfoBtn') return; + e.stopPropagation(); // evita side-effects (es. navigazione ai bordi) + window.toggleInfoPanel(window.currentPhoto); +}); + +// Chiudi pannello cliccando FUORI (non su ℹ️) +document.addEventListener('click', (e) => { + if (!isPanelOpen()) return; + const inside = infoPanel.contains(e.target); + const isBtn = e.target.id === 'modalInfoBtn'; + if (!inside && !isBtn) window.closeInfoPanel(); +}); + +// ------------------------------- +/* Auto-refresh: se cambia il media nel modal e l'info è aperto, aggiorna */ +(() => { + const mediaContainer = document.getElementById('modalMediaContainer'); + if (!mediaContainer) return; + + const refreshIfOpen = () => { + if (!isPanelOpen()) return; + const photo = window.currentPhoto; + if (photo) renderInfo(photo); + }; + + // 1) Osserva la sostituzione del media (immagine/video) nel modal + const mo = new MutationObserver(() => { + // Debounce minimo per evitare doppi render durante il replace + setTimeout(refreshIfOpen, 0); + }); + mo.observe(mediaContainer, { childList: true }); + + // 2) (Extra robustezza) ascolta le frecce se esistono + document.getElementById('modalPrev')?.addEventListener('click', () => setTimeout(refreshIfOpen, 0)); + document.getElementById('modalNext')?.addEventListener('click', () => setTimeout(refreshIfOpen, 0)); + + // 3) (Extra) tastiera ←/→ + document.addEventListener('keydown', (e) => { + if (e.key === 'ArrowLeft' || e.key === 'ArrowRight') { + setTimeout(refreshIfOpen, 0); + } + }); +})(); \ No newline at end of file diff --git a/public/js/logout.js b/public/js/logout.js new file mode 100644 index 0000000..d01d1a0 --- /dev/null +++ b/public/js/logout.js @@ -0,0 +1,153 @@ +// public/js/logout.js +// Gestione logout: chiama /auth/logout, pulisce i token, redirige e programma l'auto-logout a scadenza JWT. +// Espone: window.AppAuth.logout({redirect: true|false}) e window.AppAuth.isLoggedIn(). + +(() => { + const AUTH_LOGOUT_ENDPOINT = '/auth/logout'; + const KEYS = ['access_token', 'token', 'refresh_token']; // intercetta sia 'token' che 'access_token' + + // --- Helpers token -------------------------------------------------------- + function getAccessToken() { + try { + return ( + localStorage.getItem('access_token') || + localStorage.getItem('token') || + '' + ); + } catch { + return ''; + } + } + + function clearTokens() { + try { + KEYS.forEach(k => localStorage.removeItem(k)); + // Se usi sessionStorage per altro, rimuovi solo chiavi auth (non fare clear totale) + // sessionStorage.removeItem('my_auth_state'); // esempio + } catch {} + } + + // --- Chiamata server ------------------------------------------------------ + async function serverLogout(token) { + try { + const headers = token ? { 'Authorization': `Bearer ${token}` } : {}; + await fetch(AUTH_LOGOUT_ENDPOINT, { method: 'POST', headers }); + } catch (err) { + // In caso di errore rete, lato client puliamo comunque. + console.warn('Logout server fallito (ignoro):', err); + } + } + + // --- Redirect ------------------------------------------------------------- + function getRedirectFromBtn() { + const btn = document.querySelector('[data-logout]'); + return btn?.getAttribute('data-redirect') || '/'; + } + + function redirectAfterLogout(redirectUrl) { + const target = redirectUrl || '/'; + window.location.assign(target); + } + + // --- Auto-logout alla scadenza JWT --------------------------------------- + function decodeJwt(token) { + try { + const base64Url = token.split('.')[1]; + if (!base64Url) return null; + const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/'); + const jsonPayload = decodeURIComponent( + atob(base64).split('').map(c => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)).join('') + ); + return JSON.parse(jsonPayload); + } catch { + return null; + } + } + + let autoLogoutTimer = null; + function scheduleAutoLogout() { + clearTimeout(autoLogoutTimer); + const token = getAccessToken(); + if (!token) return; + + const payload = decodeJwt(token); + const expSec = payload?.exp; + if (!expSec) return; + + const msToExp = expSec * 1000 - Date.now(); + if (msToExp <= 0) { + // già scaduto → logout immediato + doLogout({ redirect: true }); + return; + } + + autoLogoutTimer = setTimeout(() => { + doLogout({ redirect: true }); + }, msToExp); + } + + // --- UI helpers ----------------------------------------------------------- + function setButtonsDisabled(disabled) { + document.querySelectorAll('[data-logout]').forEach(btn => { + btn.disabled = disabled; + btn.setAttribute('aria-busy', disabled ? 'true' : 'false'); + }); + } + + // --- Azione principale ---------------------------------------------------- + async function doLogout({ redirect = true } = {}) { + const token = getAccessToken(); + setButtonsDisabled(true); + + // 1) Revoca lato server (denylist) se possibile + await serverLogout(token); + + // 2) Pulizia lato client + clearTokens(); + + // 3) Chiudi eventuali media globali + try { window.player?.pause?.(); } catch {} + + // 4) Notifica globale (se vuoi ascoltarla altrove) + try { window.dispatchEvent(new CustomEvent('logout:success')); } catch {} + + // 5) Redirect + if (redirect) redirectAfterLogout(getRedirectFromBtn()); + + setButtonsDisabled(false); + } + + // --- Click handler -------------------------------------------------------- + function bindLogoutButtons() { + document.querySelectorAll('[data-logout]').forEach(btn => { + btn.addEventListener('click', async (e) => { + e.preventDefault(); + if (btn.disabled) return; // evita doppi click + await doLogout({ redirect: true }); + }); + }); + } + + // --- Public API ----------------------------------------------------------- + window.AppAuth = Object.freeze({ + logout: (opts) => doLogout(opts), + isLoggedIn: () => !!getAccessToken() + }); + + // --- Init ----------------------------------------------------------------- + function init() { + bindLogoutButtons(); + scheduleAutoLogout(); + + // Opzionale: nascondi il bottone se non loggato + document.querySelectorAll('[data-logout]').forEach(btn => { + if (!window.AppAuth.isLoggedIn()) btn.style.display = 'none'; + }); + } + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', init); + } else { + init(); + } +})(); \ No newline at end of file diff --git a/public/js/main.js b/public/js/main.js new file mode 100644 index 0000000..d05c856 --- /dev/null +++ b/public/js/main.js @@ -0,0 +1,212 @@ +// =============================== +// AVVIO +// =============================== +console.log("main.js avviato"); + +// =============================== +// PATCH: misura l'altezza reale dell'header e aggiorna --header-h +// (serve per far partire la mappa subito sotto l’header, anche su mobile) +// =============================== +(function () { + const root = document.documentElement; + const header = document.querySelector('header'); + + function setHeaderHeight() { + const h = header ? Math.round(header.getBoundingClientRect().height) : 60; + root.style.setProperty('--header-h', h + 'px'); + } + + setHeaderHeight(); + + if (window.ResizeObserver && header) { + const ro = new ResizeObserver(setHeaderHeight); + ro.observe(header); + } else { + window.addEventListener('resize', setHeaderHeight); + window.addEventListener('orientationchange', setHeaderHeight); + } + + window.addEventListener('load', setHeaderHeight); +})(); + +// =============================== +// PATCH: quando si apre la mappa (#globalMap.open) invalida le dimensioni Leaflet +// (utile perché prima era display:none; invalidateSize evita “tagli” o tile sfasati) +// =============================== +(function () { + const mapEl = document.getElementById('globalMap'); + if (!mapEl) return; + + function invalidateWhenOpen() { + if (!mapEl.classList.contains('open')) return; + // Aspetta un tick così il layout è aggiornato + setTimeout(() => { + try { + // Sostituisci con la tua variabile dell'istanza L.map, se diversa + window.leafletMapInstance?.invalidateSize(); + } catch (e) { + console.warn('invalidateSize non eseguito:', e); + } + }, 0); + } + + // 1) Osserva il cambio classe (quando aggiungi .open) + const mo = new MutationObserver((mutations) => { + if (mutations.some(m => m.attributeName === 'class')) { + invalidateWhenOpen(); + } + }); + mo.observe(mapEl, { attributes: true, attributeFilter: ['class'] }); + + // 2) Fallback: se usi il bottone #openMapBtn per aprire/chiudere + document.getElementById('openMapBtn')?.addEventListener('click', () => { + setTimeout(invalidateWhenOpen, 0); + }); +})(); + +// =============================== +// LOGIN AUTOMATICO SU INDEX +// =============================== +document.addEventListener("DOMContentLoaded", async () => { + try { + // 1) Carica config + const cfgRes = await fetch('/config'); + const cfg = await cfgRes.json(); + window.BASE_URL = cfg.baseUrl; + + // 2) Recupera token salvato + const savedToken = localStorage.getItem("token"); + + // Se non c'è token → mostra login + if (!savedToken) { + document.getElementById("loginModal").style.display = "flex"; + return; + } + + // 3) Verifica token + const ping = await fetch(`${window.BASE_URL}/photos`, { + headers: { "Authorization": "Bearer " + savedToken } + }); + + if (!ping.ok) { + // Token invalido → cancella e mostra login + localStorage.removeItem("token"); + document.getElementById("loginModal").style.display = "flex"; + return; + } + + // 4) Token valido → salva e carica gallery + window.token = savedToken; + loadPhotos(); + + } catch (err) { + console.error("Errore autenticazione:", err); + document.getElementById("loginModal").style.display = "flex"; + } +}); + +// =============================== +// VARIABILI GLOBALI +// =============================== +let currentSort = "desc"; +let currentGroup = "auto"; +let currentFilter = null; + +window.currentSort = currentSort; +window.currentGroup = currentGroup; +window.currentFilter = currentFilter; + +// =============================== +// MENU ⋮ +// =============================== +const optionsBtn = document.getElementById("optionsBtn"); +const optionsSheetEl = document.getElementById("optionsSheet"); + +optionsBtn?.addEventListener("click", () => { + optionsSheetEl?.classList.add("open"); +}); + +// =============================== +// BOTTONI OPZIONI +// =============================== +document.querySelectorAll("#optionsSheet .sheet-btn").forEach(btn => { + btn.addEventListener("click", () => { + if (btn.dataset.sort) window.currentSort = currentSort = btn.dataset.sort; + if (btn.dataset.group) window.currentGroup = currentGroup = btn.dataset.group; + if (btn.dataset.filter) window.currentFilter = currentFilter = btn.dataset.filter; + + optionsSheetEl?.classList.remove("open"); + refreshGallery(); + }); +}); + +// =============================== +// REFRESH GALLERY +// =============================== +function refreshGallery() { + console.log("Aggiornamento galleria..."); + + const data = Array.isArray(window.photosData) ? window.photosData : []; + let photos = [...data]; + + if (typeof applyFilters === 'function') photos = applyFilters(photos); + if (typeof sortByDate === 'function') photos = sortByDate(photos, currentSort); + + let sections = [{ label: 'Tutte', photos }]; + if (typeof groupByDate === 'function') sections = groupByDate(photos, currentGroup); + + if (typeof renderGallery === 'function') { + renderGallery(sections); + } +} + +window.refreshGallery = refreshGallery; + +// =============================== +// SETTINGS (⚙️) — apre admin.html +// =============================== +const settingsBtn = document.getElementById('settingsBtn'); +settingsBtn?.addEventListener('click', () => { + window.location.href = "admin.html"; +}); + +// =============================== +// LOGIN SUBMIT +// =============================== +document.getElementById("loginSubmit").addEventListener("click", async () => { + const email = document.getElementById("loginEmail").value; + const password = document.getElementById("loginPassword").value; + const errorEl = document.getElementById("loginError"); + + errorEl.textContent = ""; + + try { + const res = await fetch(`${window.BASE_URL}/auth/login`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ email, password }) + }); + + if (!res.ok) { + errorEl.textContent = "Utente o password errati"; + return; + } + + const data = await res.json(); + const token = data.token; + + // Salva token + localStorage.setItem("token", token); + window.token = token; + + // Chiudi login + document.getElementById("loginModal").style.display = "none"; + + // Carica gallery + loadPhotos(); + + } catch (err) { + console.error("Errore login:", err); + errorEl.textContent = "Errore di connessione al server"; + } +}); \ No newline at end of file diff --git a/public/js/mapGlobal.js b/public/js/mapGlobal.js new file mode 100644 index 0000000..0ce2d87 --- /dev/null +++ b/public/js/mapGlobal.js @@ -0,0 +1,224 @@ +// =============================== +// MAPPA GLOBALE — stile Google Photos Web +// - Cluster numerici che si spezzano con lo zoom +// - Click su cluster → zoom progressivo e, quando "pochi", strip in basso +// - Click su marker singolo → MODAL immediato (niente bottom) +// - Raggruppamento dinamico basato su raggio in pixel → metri +// =============================== + +window.globalMap = null; +window.globalMarkers = null; // qui sarà un MarkerClusterGroup + +document.addEventListener("DOMContentLoaded", () => { + const openBtn = document.getElementById("openMapBtn"); + if (!openBtn) { + console.error("openMapBtn non trovato nel DOM"); + return; + } + openBtn.addEventListener("click", openGlobalMap); + + // —— Parametri "alla GP" regolabili ———————————————————————————————— + const RADIUS_PX = 50; // raggio "visivo" sullo schermo per raggruppare vicini + const DISABLE_CLUSTER_AT_ZOOM = 18; // oltre questo zoom i cluster si spaccano + const OPEN_STRIP_CHILDREN_MAX = 20; // se un cluster ha <= N marker → apri strip invece di continuare a zoomare + // ———————————————————————————————————————————————————————————————— + + async function openGlobalMap() { + const mapDiv = document.getElementById("globalMap"); + const gallery = document.getElementById("gallery"); + if (!mapDiv) { + console.error("globalMap DIV non trovato"); + return; + } + + const isOpen = mapDiv.classList.contains("open"); + + // Chiudi mappa + if (isOpen) { + mapDiv.classList.remove("open"); + gallery?.classList.remove("hidden"); + window.closeBottomSheet?.(); + return; + } + + // Apri mappa e nascondi galleria + mapDiv.classList.add("open"); + gallery?.classList.add("hidden"); + + + // 👇 NUOVO: se la mappa è già stata creata in passato, riallinea l’alias + if (window.globalMap) { + window.leafletMapInstance = window.globalMap; + } + + + // Attendi dimensioni reali (evita init a height:0) + await new Promise(r => requestAnimationFrame(r)); + let tries = 0; + while (mapDiv.getBoundingClientRect().height < 50 && tries < 10) { + await new Promise(r => setTimeout(r, 30)); + tries++; + } + + // Inizializza solo la prima volta + if (window.globalMap === null) { + console.log("Inizializzo mappa Leaflet + MarkerCluster…"); + + window.globalMap = L.map("globalMap", { + zoomControl: true, + attributionControl: true + }).setView([42.5, 12.5], 6); + + L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", { + maxZoom: 19 + }).addTo(window.globalMap); + + // ✅ CLUSTER come Google Photos + window.globalMarkers = L.markerClusterGroup({ + showCoverageOnHover: false, + spiderfyOnMaxZoom: true, + disableClusteringAtZoom: DISABLE_CLUSTER_AT_ZOOM + }); + + // Listener "clusterclick": zooma oppure, quando pochi, apri strip + window.globalMarkers.on("clusterclick", (a) => { + const childMarkers = a.layer.getAllChildMarkers(); + const count = childMarkers.length; + + if (count <= OPEN_STRIP_CHILDREN_MAX || window.globalMap.getZoom() >= DISABLE_CLUSTER_AT_ZOOM - 1) { + // Converti i child markers in lista foto e apri la strip + const photos = childMarkers + .map(m => m.__photo) + .filter(Boolean); + + if (photos.length > 1) { + window.openBottomSheet?.(photos); + } else if (photos.length === 1) { + // Singola → modal diretto + openPhotoModal(photos[0]); + } + } else { + // Continua a zoomare sui bounds del cluster + window.globalMap.fitBounds(a.layer.getBounds(), { padding: [60, 60], maxZoom: DISABLE_CLUSTER_AT_ZOOM, animate: true }); + } + }); + + window.globalMap.addLayer(window.globalMarkers); + + // Disegna i marker + redrawPhotoMarkers(); + } + + // Fix dimensioni dopo apertura + setTimeout(() => window.globalMap?.invalidateSize?.(), 120); + } + + // —— Icona thumbnail per i marker ——————————————————————————————— + function createPhotoIcon(photo) { + const thumb = (typeof toAbsoluteUrl === "function") + ? toAbsoluteUrl(photo?.thub2 || photo?.thub1) + : (photo?.thub2 || photo?.thub1); + + return L.icon({ + iconUrl: thumb || "", + iconSize: [56, 56], + iconAnchor: [28, 28], + className: "photo-marker" + }); + } + + // —— Modal diretto per singola foto ———————————————————————————— + function openPhotoModal(photo) { + const thumb = (typeof toAbsoluteUrl === "function") + ? toAbsoluteUrl(photo?.thub2 || photo?.thub1 || photo?.path) + : (photo?.thub2 || photo?.thub1 || photo?.path); + + const original = (typeof toAbsoluteUrl === "function") + ? toAbsoluteUrl(photo?.path) + : photo?.path; + + window.closeBottomSheet?.(); + window.openModal?.(original, thumb, photo); + } + + // —— Raggio in metri a partire da N pixel all’zoom attuale —————————— + function radiusMetersAtZoom(latlng, px) { + if (!window.globalMap) return 0; + const p = window.globalMap.latLngToContainerPoint(latlng); + const p2 = L.point(p.x + px, p.y); + const ll2 = window.globalMap.containerPointToLatLng(p2); + return window.globalMap.distance(latlng, ll2); + } + + // —— Distanza (metri) ———————————————————————————————————————— + function distanceMeters(lat1, lng1, lat2, lng2) { + const toRad = d => d * Math.PI / 180; + const R = 6371000; + const φ1 = toRad(lat1), φ2 = toRad(lat2); + const dφ = toRad(lat2 - lat1); + const dλ = toRad(lng2 - lng1); + const a = Math.sin(dφ/2) ** 2 + + Math.cos(φ1) * Math.cos(φ2) * + Math.sin(dλ/2) ** 2; + return 2 * R * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + } + + // —— Raggruppa foto entro raggio (metri) —————————————————————— + function buildGroupByRadius(lat, lng, data, radiusM) { + return data.filter(p => { + const plat = +p?.gps?.lat; + const plng = +p?.gps?.lng; + if (!plat || !plng) return false; + return distanceMeters(lat, lng, plat, plng) <= radiusM; + }); + } + + // —— Disegno/refresh marker ———————————————————————————————— + function redrawPhotoMarkers() { + if (!window.globalMarkers || !window.globalMap) return; + + window.globalMarkers.clearLayers(); + const data = Array.isArray(window.photosData) ? window.photosData : []; + + data.forEach(photo => { + const lat = +photo?.gps?.lat; + const lng = +photo?.gps?.lng; + if (!lat || !lng) return; + + const marker = L.marker([lat, lng], { + icon: createPhotoIcon(photo), + title: photo?.name || "" + }); + + // Alleghiamo la foto al marker per recuperarla in eventi cluster + marker.__photo = photo; + + // Click su marker: calcola raggio dinamico e apri strip o modal + marker.on("click", () => { + const here = L.latLng(lat, lng); + const radiusM = radiusMetersAtZoom(here, RADIUS_PX); + const dataAll = Array.isArray(window.photosData) ? window.photosData : []; + const group = buildGroupByRadius(lat, lng, dataAll, radiusM); + + if (group.length > 1) { + window.openBottomSheet?.(group); + } else if (group.length === 1) { + openPhotoModal(group[0]); + } else { + openPhotoModal(photo); + } + }); + + window.globalMarkers.addLayer(marker); + }); + } + + // Se la gallery si aggiorna (loadPhotos), ridisegna marker + const originalRefresh = window.refreshGallery; + window.refreshGallery = function wrappedRefreshGallery(...args) { + try { originalRefresh?.apply(this, args); } catch (_) {} + if (window.globalMap && window.globalMarkers) { + redrawPhotoMarkers(); + } + }; +}); \ No newline at end of file diff --git a/public/js/modal.js b/public/js/modal.js new file mode 100644 index 0000000..a350f85 --- /dev/null +++ b/public/js/modal.js @@ -0,0 +1,350 @@ +// =============================== +// MODALE (FOTO + VIDEO) — avanzato con navigazione e preload +// - Sostituisce il contenuto, non accumula +// - Chiude la bottom-zone quando si apre +// - Prev/Next (←/→ e click ai bordi), preload 3+3 +// - Pulsante INFO (ℹ️) riportato dentro il modal con toggle affidabile +// =============================== + +const modal = document.getElementById('modal'); +const modalClose = document.getElementById('modalClose'); + +window.currentPhoto = null; // usato anche da infoPanel +window.modalList = []; // lista corrente per navigazione +window.modalIndex = 0; // indice corrente nella lista + +// Frecce visibili +const modalPrev = document.getElementById('modalPrev'); +const modalNext = document.getElementById('modalNext'); + +// =============================== +// Stato/Helper Info Panel (toggle affidabile) +// =============================== +let infoOpen = false; // stato interno affidabile + +function getInfoPanel() { + return document.getElementById('infoPanel'); +} + +function isInfoOpen() { + return infoOpen; +} + +function openInfo(photo) { + // Prova API esplicita, altrimenti fallback a toggle + try { + if (typeof window.openInfoPanel === 'function') { + window.openInfoPanel(photo); + } else if (typeof window.toggleInfoPanel === 'function') { + window.toggleInfoPanel(photo); + } + } catch {} + + infoOpen = true; + const panel = getInfoPanel(); + panel?.classList.add('open'); + panel?.setAttribute('aria-hidden', 'false'); + panel?.setAttribute('data-open', '1'); + document.getElementById('modalInfoBtn')?.classList.add('active'); +} + +function closeInfo() { + // Prova API esplicita, altrimenti fallback a toggle (senza argomento) + try { + if (typeof window.closeInfoPanel === 'function') { + window.closeInfoPanel(); + } else if (typeof window.toggleInfoPanel === 'function') { + window.toggleInfoPanel(); + } + } catch {} + + infoOpen = false; + const panel = getInfoPanel(); + panel?.classList.remove('open'); + panel?.setAttribute('aria-hidden', 'true'); + panel?.setAttribute('data-open', '0'); + document.getElementById('modalInfoBtn')?.classList.remove('active'); +} + +function toggleInfo(photo) { + if (isInfoOpen()) closeInfo(); + else openInfo(photo); +} + +// =============================== +// Utility MIME / media +// =============================== +function isProbablyVideo(photo, srcOriginal) { + const mime = String(photo?.mime_type || '').toLowerCase(); + if (mime.startsWith('video/')) return true; + return /\.(mp4|m4v|webm|mov|qt|avi|mkv)$/i.test(String(srcOriginal || '')); +} + +function guessVideoMime(photo, srcOriginal) { + let t = String(photo?.mime_type || '').toLowerCase(); + if (t && t !== 'application/octet-stream') return t; + const src = String(srcOriginal || ''); + if (/\.(mp4|m4v)$/i.test(src)) return 'video/mp4'; + if (/\.(webm)$/i.test(src)) return 'video/webm'; + if (/\.(mov|qt)$/i.test(src)) return 'video/quicktime'; + if (/\.(avi)$/i.test(src)) return 'video/x-msvideo'; + if (/\.(mkv)$/i.test(src)) return 'video/x-matroska'; + return ''; +} + +function createVideoElement(srcOriginal, srcPreview, photo) { + const video = document.createElement('video'); + video.controls = true; + video.playsInline = true; // iOS: evita fullscreen nativo + video.setAttribute('webkit-playsinline', ''); // compat iOS storici + video.preload = 'metadata'; + video.poster = srcPreview || ''; + video.style.maxWidth = '100%'; + video.style.maxHeight = '100%'; + video.style.objectFit = 'contain'; + + const source = document.createElement('source'); + source.src = srcOriginal; + const type = guessVideoMime(photo, srcOriginal); + if (type) source.type = type; + video.appendChild(source); + + video.addEventListener('loadedmetadata', () => { + try { video.currentTime = 0.001; } catch (_) {} + console.log('[video] loadedmetadata', { w: video.videoWidth, h: video.videoHeight, dur: video.duration }); + }); + + video.addEventListener('error', () => { + const code = video.error && video.error.code; + console.warn('[video] error code:', code, 'type:', type, 'src:', srcOriginal); + const msg = document.createElement('div'); + msg.style.padding = '12px'; + msg.style.color = '#fff'; + msg.style.background = 'rgba(0,0,0,0.6)'; + msg.style.borderRadius = '8px'; + msg.innerHTML = ` + Impossibile riprodurre questo video nel browser. + ${code === 4 ? 'Formato/codec non supportato (es. HEVC/H.265 su Chrome/Edge).' : 'Errore durante il caricamento.'} +

+ Suggerimenti: + + `; + const container = document.getElementById('modalMediaContainer'); + container && container.appendChild(msg); + }); + + // Evita di far scattare la navigazione "ai bordi" + video.addEventListener('click', (e) => e.stopPropagation()); + + return video; +} + +function createImageElement(srcOriginal, srcPreview) { + const img = document.createElement('img'); + img.src = srcPreview || srcOriginal || ''; + img.style.maxWidth = '100%'; + img.style.maxHeight = '100%'; + img.style.objectFit = 'contain'; + + // Progressive loading: preview → fullres + if (srcPreview && srcOriginal && srcPreview !== srcOriginal) { + const full = new Image(); + full.src = srcOriginal; + full.onload = () => { img.src = srcOriginal; }; + } + return img; +} + +// =============================== +// Helpers per URL assoluti +// =============================== +function absUrl(path) { + return (typeof toAbsoluteUrl === 'function') ? toAbsoluteUrl(path) : path; +} + +function mediaUrlsFromPhoto(photo) { + const original = absUrl(photo?.path); + const preview = absUrl(photo?.thub2 || photo?.thub1 || photo?.path); + return { original, preview }; +} + +// =============================== +// PRELOAD ±N (solo immagini; per i video: poster/preview) +// =============================== +function preloadNeighbors(N = 3) { + const list = window.modalList || []; + const idx = window.modalIndex || 0; + + for (let offset = 1; offset <= N; offset++) { + const iPrev = idx - offset; + const iNext = idx + offset; + [iPrev, iNext].forEach(i => { + const p = list[i]; + if (!p) return; + const { original, preview } = mediaUrlsFromPhoto(p); + const isVideo = String(p?.mime_type || '').toLowerCase().startsWith('video/'); + const src = isVideo ? (preview || original) : original; + if (!src) return; + const img = new Image(); + img.src = src; + }); + } +} + +// =============================== +// Core: imposta contenuto modal +// =============================== +function setModalContent(photo, srcOriginal, srcPreview) { + const container = document.getElementById('modalMediaContainer'); + container.innerHTML = ''; + window.currentPhoto = photo; + + const isVideo = isProbablyVideo(photo, srcOriginal); + console.log('[openModal]', { isVideo, mime: photo?.mime_type, srcOriginal, srcPreview }); + + if (isVideo) { + const video = createVideoElement(srcOriginal, srcPreview, photo); + container.appendChild(video); + } else { + const img = createImageElement(srcOriginal, srcPreview); + container.appendChild(img); + } + + // Pulsante INFO (ℹ️) dentro il modal — toggle vero + const infoBtn = document.createElement('button'); + infoBtn.id = 'modalInfoBtn'; + infoBtn.className = 'modal-info-btn'; + infoBtn.type = 'button'; + infoBtn.setAttribute('aria-label', 'Dettagli'); + infoBtn.textContent = 'ℹ️'; + container.appendChild(infoBtn); + + infoBtn.addEventListener('click', (e) => { + e.stopPropagation(); // non far scattare navigazione + toggleInfo(window.currentPhoto); + }); +} + +// =============================== +// API base: open/close modal (mantiene sostituzione contenuto) +// =============================== +function openModal(srcOriginal, srcPreview, photo) { + // Chiudi sempre la strip prima di aprire + window.closeBottomSheet?.(); + + setModalContent(photo, srcOriginal, srcPreview); + modal.classList.add('open'); + modal.setAttribute('aria-hidden', 'false'); + document.body.style.overflow = 'hidden'; +} + +function closeModal() { + // Chiudi anche l'info se aperto + if (isInfoOpen()) closeInfo(); + + const v = document.querySelector('#modal video'); + if (v) { + try { v.pause(); } catch (_) {} + v.removeAttribute('src'); + while (v.firstChild) v.removeChild(v.firstChild); + try { v.load(); } catch (_) {} + } + const container = document.getElementById('modalMediaContainer'); + if (container) container.innerHTML = ''; + modal.classList.remove('open'); + modal.setAttribute('aria-hidden', 'true'); + document.body.style.overflow = ''; + + // Nascondi frecce alla chiusura (così non "rimangono" visibili) + try { + modalPrev?.classList.add('hidden'); + modalNext?.classList.add('hidden'); + } catch {} +} + +// X: stopPropagation + chiudi +modalClose?.addEventListener('click', (e) => { e.stopPropagation(); closeModal(); }); + +// Backdrop: chiudi cliccando fuori +modal.addEventListener('click', (e) => { if (e.target === modal) closeModal(); }); + +// =============================== +// Navigazione: lista + indice + prev/next + click ai bordi + tastiera +// =============================== +function openAt(i) { + const list = window.modalList || []; + if (!list[i]) return; + window.modalIndex = i; + const photo = list[i]; + const { original, preview } = mediaUrlsFromPhoto(photo); + + // Se l'info è aperto, aggiorna i contenuti per la nuova foto + if (isInfoOpen()) { + openInfo(photo); + } + + openModal(original, preview, photo); // sostituisce contenuto + preloadNeighbors(3); + updateArrows(); +} + +window.openModalFromList = function(list, index) { + window.modalList = Array.isArray(list) ? list : []; + window.modalIndex = Math.max(0, Math.min(index || 0, window.modalList.length - 1)); + openAt(window.modalIndex); +}; + +function showPrev() { if (window.modalIndex > 0) openAt(window.modalIndex - 1); } +function showNext() { if (window.modalIndex < (window.modalList.length - 1)) openAt(window.modalIndex + 1); } + +// Tastiera +document.addEventListener('keydown', (e) => { + if (!modal.classList.contains('open')) return; + if (e.key === 'ArrowLeft') { e.preventDefault(); showPrev(); } + if (e.key === 'ArrowRight') { e.preventDefault(); showNext(); } +}); + +// Click ai bordi del modal: sinistra=prev, destra=next (ignora controlli) +modal.addEventListener('click', (e) => { + if (!modal.classList.contains('open')) return; + + // Ignora click sui controlli + if (e.target.closest('.modal-info-btn, .modal-close, .modal-nav-btn')) return; + + if (e.target === modal) return; // già gestito per chiusura + + const rect = modal.getBoundingClientRect(); + const x = e.clientX - rect.left; + const side = x / rect.width; + if (side < 0.25) showPrev(); + else if (side > 0.75) showNext(); +}); + +// Esporta API base (per compatibilità con codice esistente) +window.openModal = openModal; +window.closeModal = closeModal; + +// =============================== +// FRECCE DI NAVIGAZIONE < > +// =============================== +function updateArrows() { + if (!modalPrev || !modalNext) return; + const len = (window.modalList || []).length; + const i = window.modalIndex || 0; + + // Mostra frecce solo se ci sono almeno 2 elementi + const show = len > 1; + modalPrev.classList.toggle('hidden', !show); + modalNext.classList.toggle('hidden', !show); + + // Disabilita ai bordi (no wrap) + modalPrev.classList.toggle('disabled', i <= 0); + modalNext.classList.toggle('disabled', i >= len - 1); +} + +// Click sulle frecce: non propagare (evita conflitti col click sui bordi) +modalPrev?.addEventListener('click', (e) => { e.stopPropagation(); showPrev(); updateArrows(); }); +modalNext?.addEventListener('click', (e) => { e.stopPropagation(); showNext(); updateArrows(); }); \ No newline at end of file diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0092.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0092.JPG new file mode 100644 index 0000000..fb44c2e Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0092.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0099.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0099.JPG new file mode 100644 index 0000000..bbf6b8f Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0099.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0100.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0100.JPG new file mode 100644 index 0000000..cadc7d5 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0100.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0102.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0102.JPG new file mode 100644 index 0000000..4401f38 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0102.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0103.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0103.JPG new file mode 100644 index 0000000..78252b2 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0103.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0104.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0104.JPG new file mode 100644 index 0000000..7938c88 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0104.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0106.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0106.JPG new file mode 100644 index 0000000..6f255a3 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0106.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0107.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0107.JPG new file mode 100644 index 0000000..721f942 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0107.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0108.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0108.JPG new file mode 100644 index 0000000..81656da Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0108.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0109.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0109.JPG new file mode 100644 index 0000000..d9cf826 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0109.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0110.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0110.JPG new file mode 100644 index 0000000..93cfa80 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0110.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0112.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0112.JPG new file mode 100644 index 0000000..897ae3d Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0112.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0113.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0113.JPG new file mode 100644 index 0000000..8a5235b Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0113.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0114.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0114.JPG new file mode 100644 index 0000000..1537b13 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0114.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0116.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0116.JPG new file mode 100644 index 0000000..6acf3c5 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0116.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0119.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0119.JPG new file mode 100644 index 0000000..0156bcb Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0119.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0120.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0120.JPG new file mode 100644 index 0000000..6ecad02 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0120.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0122.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0122.JPG new file mode 100644 index 0000000..b33b484 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0122.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0123.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0123.JPG new file mode 100644 index 0000000..c3088f0 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0123.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0124.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0124.JPG new file mode 100644 index 0000000..ff5c90c Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0124.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0125.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0125.JPG new file mode 100644 index 0000000..a741447 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0125.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0126.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0126.JPG new file mode 100644 index 0000000..71f70c1 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0126.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0133.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0133.JPG new file mode 100644 index 0000000..c8001b7 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0133.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0134.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0134.JPG new file mode 100644 index 0000000..79ff160 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0134.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0135.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0135.JPG new file mode 100644 index 0000000..ca95e34 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0135.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0136.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0136.JPG new file mode 100644 index 0000000..92f1d1f Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0136.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0137.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0137.JPG new file mode 100644 index 0000000..0b122c3 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0137.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0138.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0138.JPG new file mode 100644 index 0000000..534ffe8 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0138.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0139.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0139.JPG new file mode 100644 index 0000000..f47c0ff Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0139.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0140.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0140.JPG new file mode 100644 index 0000000..41b9b20 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0140.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0141.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0141.JPG new file mode 100644 index 0000000..d4c4226 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0141.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0143.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0143.JPG new file mode 100644 index 0000000..db5aa75 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0143.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0145.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0145.JPG new file mode 100644 index 0000000..3fb3078 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0145.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0146.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0146.JPG new file mode 100644 index 0000000..7cc88a8 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0146.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0147.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0147.JPG new file mode 100644 index 0000000..e7bdd87 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0147.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0148.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0148.JPG new file mode 100644 index 0000000..c8994d3 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0148.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0149.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0149.JPG new file mode 100644 index 0000000..ccc11c8 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0149.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0150.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0150.JPG new file mode 100644 index 0000000..a662369 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0150.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0152.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0152.JPG new file mode 100644 index 0000000..1ee886d Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0152.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0153.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0153.JPG new file mode 100644 index 0000000..777a4c0 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0153.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0154.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0154.JPG new file mode 100644 index 0000000..e5a14b3 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0154.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0155.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0155.JPG new file mode 100644 index 0000000..f61fa2b Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0155.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0156.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0156.JPG new file mode 100644 index 0000000..4790319 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0156.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0157.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0157.JPG new file mode 100644 index 0000000..47d96af Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0157.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0160.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0160.JPG new file mode 100644 index 0000000..6011b00 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0160.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0162.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0162.JPG new file mode 100644 index 0000000..3c16a00 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0162.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0163.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0163.JPG new file mode 100644 index 0000000..dcdbd55 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0163.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0164.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0164.JPG new file mode 100644 index 0000000..946059d Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0164.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0165.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0165.JPG new file mode 100644 index 0000000..1e5cb4b Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0165.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0166.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0166.JPG new file mode 100644 index 0000000..6e639c7 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0166.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0167.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0167.JPG new file mode 100644 index 0000000..4094874 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0167.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0170.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0170.JPG new file mode 100644 index 0000000..ebe7c55 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0170.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0171.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0171.JPG new file mode 100644 index 0000000..fa1e8e7 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0171.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0172.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0172.JPG new file mode 100644 index 0000000..08b8197 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0172.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0174.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0174.JPG new file mode 100644 index 0000000..ed83ac3 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0174.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0175.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0175.JPG new file mode 100644 index 0000000..e1d4b51 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0175.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0176.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0176.JPG new file mode 100644 index 0000000..e8af76c Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0176.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0177.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0177.JPG new file mode 100644 index 0000000..f3ea4c8 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0177.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0178.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0178.JPG new file mode 100644 index 0000000..0809fad Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0178.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0179.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0179.JPG new file mode 100644 index 0000000..f85e317 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0179.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0180.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0180.JPG new file mode 100644 index 0000000..d992ed6 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0180.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0182.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0182.JPG new file mode 100644 index 0000000..3e395d4 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0182.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0183.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0183.JPG new file mode 100644 index 0000000..9d8bc9c Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0183.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0185.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0185.JPG new file mode 100644 index 0000000..5ea5f9e Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0185.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0188.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0188.JPG new file mode 100644 index 0000000..2312168 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0188.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0190.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0190.JPG new file mode 100644 index 0000000..2dada06 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0190.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0193.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0193.JPG new file mode 100644 index 0000000..6398d18 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0193.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0203.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0203.JPG new file mode 100644 index 0000000..3b2699e Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0203.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0204.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0204.JPG new file mode 100644 index 0000000..744c2e7 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0204.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0206.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0206.JPG new file mode 100644 index 0000000..93f5a2d Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0206.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0207.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0207.JPG new file mode 100644 index 0000000..6af3504 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0207.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0208.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0208.JPG new file mode 100644 index 0000000..82db55d Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0208.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0209.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0209.JPG new file mode 100644 index 0000000..fee5f53 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0209.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0211.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0211.JPG new file mode 100644 index 0000000..9d35dd8 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0211.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0212.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0212.JPG new file mode 100644 index 0000000..264d1e0 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0212.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0214.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0214.JPG new file mode 100644 index 0000000..6ac4dbe Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0214.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0215.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0215.JPG new file mode 100644 index 0000000..e894612 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0215.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0216.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0216.JPG new file mode 100644 index 0000000..3ac2b2b Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0216.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0217.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0217.JPG new file mode 100644 index 0000000..5fe02c1 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0217.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0218.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0218.JPG new file mode 100644 index 0000000..4e2211a Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0218.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0223.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0223.JPG new file mode 100644 index 0000000..7a8b3df Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0223.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0227.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0227.JPG new file mode 100644 index 0000000..5dcf97b Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0227.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0228.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0228.JPG new file mode 100644 index 0000000..8aac3ae Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0228.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0229.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0229.JPG new file mode 100644 index 0000000..139cc35 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0229.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0232.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0232.JPG new file mode 100644 index 0000000..725e489 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0232.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0233.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0233.JPG new file mode 100644 index 0000000..d8f588e Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0233.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0234.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0234.JPG new file mode 100644 index 0000000..71c1f0b Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0234.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0235.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0235.JPG new file mode 100644 index 0000000..caf3d16 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0235.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0237.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0237.JPG new file mode 100644 index 0000000..57e5683 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0237.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0241.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0241.JPG new file mode 100644 index 0000000..c8a254d Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0241.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0242.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0242.JPG new file mode 100644 index 0000000..0c8414a Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0242.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0243.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0243.JPG new file mode 100644 index 0000000..a5749a6 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0243.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0244.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0244.JPG new file mode 100644 index 0000000..749e6ab Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0244.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0245.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0245.JPG new file mode 100644 index 0000000..b79ede8 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0245.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0246.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0246.JPG new file mode 100644 index 0000000..fe7d69a Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0246.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0247.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0247.JPG new file mode 100644 index 0000000..cb6f053 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0247.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0248.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0248.JPG new file mode 100644 index 0000000..4063c0d Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0248.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0249.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0249.JPG new file mode 100644 index 0000000..2e872f3 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0249.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/VID_20260221_095917.mp4 b/public/photos/Fabio/original/2017Irlanda19-29ago/VID_20260221_095917.mp4 new file mode 100644 index 0000000..54b7938 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/VID_20260221_095917.mp4 differ diff --git a/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_100423.jpg b/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_100423.jpg new file mode 100644 index 0000000..84e227b Binary files /dev/null and b/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_100423.jpg differ diff --git a/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_100428.jpg b/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_100428.jpg new file mode 100644 index 0000000..bebd335 Binary files /dev/null and b/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_100428.jpg differ diff --git a/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_100449.jpg b/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_100449.jpg new file mode 100644 index 0000000..03cfd8e Binary files /dev/null and b/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_100449.jpg differ diff --git a/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_100532.jpg b/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_100532.jpg new file mode 100644 index 0000000..7f80413 Binary files /dev/null and b/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_100532.jpg differ diff --git a/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_100557.jpg b/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_100557.jpg new file mode 100644 index 0000000..11c5fbf Binary files /dev/null and b/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_100557.jpg differ diff --git a/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_100637.jpg b/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_100637.jpg new file mode 100644 index 0000000..5a720a3 Binary files /dev/null and b/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_100637.jpg differ diff --git a/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_100703.jpg b/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_100703.jpg new file mode 100644 index 0000000..17c8898 Binary files /dev/null and b/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_100703.jpg differ diff --git a/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_100736.jpg b/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_100736.jpg new file mode 100644 index 0000000..6a01a80 Binary files /dev/null and b/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_100736.jpg differ diff --git a/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_104441.jpg b/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_104441.jpg new file mode 100644 index 0000000..b2dda4b Binary files /dev/null and b/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_104441.jpg differ diff --git a/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_104444.jpg b/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_104444.jpg new file mode 100644 index 0000000..bdeae55 Binary files /dev/null and b/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_104444.jpg differ diff --git a/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_140046.jpg b/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_140046.jpg new file mode 100644 index 0000000..1405bfc Binary files /dev/null and b/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_140046.jpg differ diff --git a/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_140118.jpg b/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_140118.jpg new file mode 100644 index 0000000..941d712 Binary files /dev/null and b/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_140118.jpg differ diff --git a/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_140120.jpg b/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_140120.jpg new file mode 100644 index 0000000..5133b01 Binary files /dev/null and b/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_140120.jpg differ diff --git a/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_140215.jpg b/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_140215.jpg new file mode 100644 index 0000000..d6c7fac Binary files /dev/null and b/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_140215.jpg differ diff --git a/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_140246.jpg b/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_140246.jpg new file mode 100644 index 0000000..a208376 Binary files /dev/null and b/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_140246.jpg differ diff --git a/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_173903.jpg b/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_173903.jpg new file mode 100644 index 0000000..904762d Binary files /dev/null and b/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_173903.jpg differ diff --git a/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_173923.jpg b/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_173923.jpg new file mode 100644 index 0000000..219bd81 Binary files /dev/null and b/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_173923.jpg differ diff --git a/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_180652.jpg b/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_180652.jpg new file mode 100644 index 0000000..3781e6c Binary files /dev/null and b/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_180652.jpg differ diff --git a/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_180710.jpg b/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_180710.jpg new file mode 100644 index 0000000..a63cfd3 Binary files /dev/null and b/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_180710.jpg differ diff --git a/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_180723.jpg b/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_180723.jpg new file mode 100644 index 0000000..e84173d Binary files /dev/null and b/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_180723.jpg differ diff --git a/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_181011.jpg b/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_181011.jpg new file mode 100644 index 0000000..21e2d24 Binary files /dev/null and b/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_181011.jpg differ diff --git a/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_181034.jpg b/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_181034.jpg new file mode 100644 index 0000000..31e4f6b Binary files /dev/null and b/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_181034.jpg differ