// routes/activities.js const express = require('express'); const router = express.Router(); const path = require('path'); const fsp = require('fs/promises'); const db = require('../db/knex'); const wss = require('../ws-server'); const normalizeActivity = require('../helpers/normalizeActivity'); // ----------------------------------------------------- // File helpers hard delete attività // ----------------------------------------------------- const PUBLIC_ROOT = path.resolve(__dirname, '..', 'public'); const ACTIVITIES_ROOT = path.join(PUBLIC_ROOT, 'activities'); const DEL_ACTIVITIES_ROOT = path.join(PUBLIC_ROOT, 'del_activities'); function safeJoin(base, ...parts) { const resolvedBase = path.resolve(base); const resolved = path.resolve(base, ...parts); if (!resolved.startsWith(resolvedBase)) { throw new Error(`Path non sicuro: ${resolved}`); } return resolved; } function getOriginalActivityPath(row) { return safeJoin( ACTIVITIES_ROOT, row.user, 'original', row.cartella || '', row.file_name ); } function getDeletedActivityPath(row) { return safeJoin( DEL_ACTIVITIES_ROOT, row.user, 'original', row.cartella || '', row.file_name ); } async function pathExists(filePath) { try { await fsp.access(filePath); return true; } catch { return false; } } async function moveFileSafe(src, dst) { if (!src || !dst) return false; const exists = await pathExists(src); if (!exists) { console.warn(`⚠️ File attività non trovato, skip move: ${src}`); return false; } // Crea automaticamente public/del_activities//original// await fsp.mkdir(path.dirname(dst), { recursive: true }); try { await fsp.rename(src, dst); return true; } catch (err) { // fallback se source/destination sono su filesystem diversi if (err.code === 'EXDEV') { await fsp.copyFile(src, dst); await fsp.unlink(src); return true; } throw err; } } // ----------------------------------------------------- // POST /activities // Salva attività inviata dallo scanner locale // ----------------------------------------------------- router.post('/', async (req, res) => { const a = req.body; const start = Date.now(); console.log(`📥 [POST /activities] Richiesta at=${new Date().toISOString()}`); console.log("📥 Body:", a); try { console.log(`📸 [DB INSERT] Salvo activity id=${a.id} user=${a.user}`); await db('activities') .insert({ id: a.id, user: a.user, cartella: a.cartella, file_name: a.file_name, file_extension: a.file_extension, format: a.format, display_name: a.display_name, route_name: a.route_name, title: a.title, description: a.description, notes: a.notes, sport: a.sport, activity_type: a.activity_type, device_manufacturer: a.device_manufacturer, device_model: a.device_model, source_app: a.source_app, start_time_millis: a.start_time_millis, end_time_millis: a.end_time_millis, duration_millis: a.duration_millis, distance_meters: a.distance_meters, elevation_gain_meters: a.elevation_gain_meters, elevation_loss_meters: a.elevation_loss_meters, min_elevation_meters: a.min_elevation_meters, max_elevation_meters: a.max_elevation_meters, file_distance_meters: a.file_distance_meters, file_elevation_gain_meters: a.file_elevation_gain_meters, computed_distance_meters: a.computed_distance_meters, computed_elevation_gain_meters: a.computed_elevation_gain_meters, average_speed_mps: a.average_speed_mps, max_speed_mps: a.max_speed_mps, average_pace_sec_per_km: a.average_pace_sec_per_km, average_heart_rate: a.average_heart_rate, max_heart_rate: a.max_heart_rate, average_cadence: a.average_cadence, max_cadence: a.max_cadence, calories: a.calories, power_avg: a.power_avg, power_max: a.power_max, point_count: a.point_count, segment_count: a.segment_count, lap_count: a.lap_count, min_latitude: a.min_latitude, max_latitude: a.max_latitude, min_longitude: a.min_longitude, max_longitude: a.max_longitude, center_latitude: a.center_latitude, center_longitude: a.center_longitude, size_bytes: a.size_bytes, last_modified_millis: a.last_modified_millis, date_added_millis: a.date_added_millis, date_modified_millis: a.date_modified_millis, last_scan_millis: a.last_scan_millis, is_favorite: a.is_favorite, is_hidden: a.is_hidden, rating: a.rating, tags: a.tags ? JSON.stringify(a.tags) : null, track_preview: a.track_preview ? JSON.stringify(a.track_preview) : null, scan_status: a.scan_status, scan_error: a.scan_error, diagnostics: a.diagnostics ? JSON.stringify(a.diagnostics) : null, location: a.location ? JSON.stringify(a.location) : null, path: a.path, fast_hash: a.fast_hash, deleted_at: a.deleted_at || null, created_at: new Date().toISOString(), updated_at: new Date().toISOString() }) .onConflict('id') .merge(); wss.broadcastToUser(a.user, { type: "added", id: a.id, entity: "activity" }); console.log(`✅ [POST /activities] Activity id=${a.id} salvata durata=${Date.now() - start}ms`); res.json({ ok: true }); } catch (err) { console.error("❌ Errore POST /activities:", err); res.status(500).json({ error: err.message }); } }); // ----------------------------------------------------- // GET /activities/changes // Progressive sync // ----------------------------------------------------- router.get('/changes', async (req, res) => { const user = req.user.name; const since = req.query.since; const start = Date.now(); console.log( `🔄 [SYNC SINCE] GET /activities/changes user=${user} since=${since} at=${new Date().toISOString()}` ); if (!since) { console.log("⚠️ [SYNC SINCE] Richiesta senza ?since"); return res.status(400).json({ error: "Missing ?since=timestamp" }); } try { let rows; if (user === "Admin") { rows = await db("activities") .where(builder => { builder .where("created_at", ">", since) .orWhere("updated_at", ">", since); }) .orderBy("updated_at", "desc"); } else { rows = await db("activities") .whereIn("user", [user, "Common"]) .andWhere(builder => { builder .where("created_at", ">", since) .orWhere("updated_at", ">", since); }) .orderBy("updated_at", "desc"); } console.log( `🔄 [SYNC SINCE] Risposta: ${rows.length} attività durata=${Date.now() - start}ms` ); res.json(rows.map(normalizeActivity)); } catch (err) { console.error("❌ Errore GET /activities/changes:", err); res.status(500).json({ error: err.message }); } }); // ----------------------------------------------------- // GET /activities/byIds // ----------------------------------------------------- router.get('/byIds', async (req, res) => { const ids = req.query.id; const users = Array.isArray(req.query.user) ? req.query.user : [req.query.user]; const start = Date.now(); console.log(`📤 [GET /activities/byIds] at=${new Date().toISOString()}`); console.log("📤 ids:", ids); console.log("📤 users:", users); if (!ids) return res.json([]); const list = Array.isArray(ids) ? ids : [ids]; try { let rows; if (users.includes("Admin")) { rows = await db('activities').whereIn('id', list); } else { rows = await db('activities') .whereIn('id', list) .whereIn('user', users); } console.log( `📤 [GET /activities/byIds] trovate ${rows.length} attività durata=${Date.now() - start}ms` ); res.json(rows.map(normalizeActivity)); } catch (err) { console.error("❌ Errore /activities/byIds:", err); res.status(500).json({ error: err.message }); } }); // ----------------------------------------------------- // GET /activities // Full sync // ----------------------------------------------------- router.get('/', async (req, res) => { const loggedUser = req.user?.name; const start = Date.now(); console.log( `📤 [FULL SYNC] GET /activities da=${loggedUser} at=${new Date().toISOString()}` ); try { let rows; if (loggedUser === "Admin") { rows = await db('activities') .select('*') .orderBy('start_time_millis', 'desc'); } else { rows = await db('activities') .whereIn('user', [loggedUser, "Common"]) .orderBy('start_time_millis', 'desc'); } console.log( `📤 [FULL SYNC] Risposta: ${rows.length} attività durata=${Date.now() - start}ms` ); res.json(rows.map(normalizeActivity)); } catch (err) { console.error("❌ Errore GET /activities:", err); res.status(500).json({ error: err.message }); } }); // ----------------------------------------------------- // POST /activities/initDB // Reset DB attività // - Admin: cancella tutte le attività // - Utente normale: cancella solo le proprie // ----------------------------------------------------- router.post('/initDB', async (req, res) => { try { const loggedUser = req.user?.name; if (!loggedUser) { return res.status(401).json({ ok: false, error: "Unauthorized" }); } console.log(`🧹 [POST /activities/initDB] richiesto da=${loggedUser}`); if (loggedUser === "Admin") { console.log("🧹 Admin → cancello tutte le attività"); await db('activities').del(); await db('deleted_hard_activities').del(); await db('sync_done_activities').del(); return res.json({ ok: true, scope: "all", user: loggedUser, message: "DB attività svuotato completamente" }); } console.log(`🧹 User=${loggedUser} → cancello solo le sue attività`); await db('activities') .where({ user: loggedUser }) .del(); await db('deleted_hard_activities') .where({ user: loggedUser }) .del(); await db('sync_done_activities').del(); res.json({ ok: true, scope: "user", user: loggedUser, message: `DB attività svuotato per user=${loggedUser}` }); } catch (err) { console.error("❌ Errore POST /activities/initDB:", err); res.status(500).json({ ok: false, error: err.message }); } }); // ----------------------------------------------------- // POST /activities/initDBuser // Reset DB attività per utente specifico // Solo Admin // ----------------------------------------------------- router.post('/initDBuser', async (req, res) => { try { const loggedUser = req.user?.name; if (loggedUser !== "Admin") { return res.status(403).json({ ok: false, error: "Forbidden", loggedUser }); } const targetUser = req.body?.user || req.query?.user || req.body?.name || req.query?.name; if (!targetUser) { return res.status(400).json({ ok: false, error: "Missing user" }); } console.log( `🧹 [POST /activities/initDBuser] Admin=${loggedUser} target=${targetUser}` ); await db('activities') .where({ user: targetUser }) .del(); await db('deleted_hard_activities') .where({ user: targetUser }) .del(); await db('sync_done_activities').del(); res.json({ ok: true, user: targetUser, message: `DB attività svuotato per user=${targetUser}` }); } catch (err) { console.error("❌ Errore POST /activities/initDBuser:", err); res.status(500).json({ ok: false, error: err.message }); } }); // ----------------------------------------------------- // GET /activities/deleted_hard // Deve stare PRIMA di /:id // ----------------------------------------------------- router.get('/deleted_hard', async (req, res) => { const since = req.query.since; const user = req.user.name; try { let rows; if (user === "Admin") { rows = await db('deleted_hard_activities') .where('deleted_at', '>', since); } else { rows = await db('deleted_hard_activities') .where('deleted_at', '>', since) .where({ user }); } res.json({ deleted: rows }); } catch (err) { console.error("❌ Errore GET /activities/deleted_hard:", err); res.status(500).json({ error: err.message }); } }); // ----------------------------------------------------- // POST /activities/sync_done // ----------------------------------------------------- router.post('/sync_done', async (req, res) => { try { await db('sync_done_activities').insert({ session_id: req.body.session_id, device_id: req.body.device_id, last_sync: req.body.last_sync }); res.json({ ok: true }); } catch (err) { res.status(500).json({ error: err.message }); } }); // ----------------------------------------------------- // POST /activities/:id/toggle-soft // Toggle soft delete attività per ID // - Se deleted_at è null → soft delete // - Se deleted_at è valorizzato → restore // ----------------------------------------------------- router.post('/:id/toggle-soft', async (req, res) => { const id = req.params.id; const loggedUser = req.user?.name; const start = Date.now(); try { if (!loggedUser) { return res.status(401).json({ ok: false, error: "Unauthorized" }); } console.log(`🔁 [TOGGLE SOFT /activities/${id}] richiesto da=${loggedUser}`); let row; if (loggedUser === "Admin") { row = await db('activities') .where({ id }) .first(); } else { row = await db('activities') .where({ id, user: loggedUser }) .first(); } if (!row) { return res.status(404).json({ ok: false, error: "Activity not found or not allowed", id }); } const now = new Date().toISOString(); const nowMs = Date.now(); const nextDeletedAt = row.deleted_at ? null : now; await db('activities') .where({ id }) .update({ deleted_at: nextDeletedAt, updated_at: now, date_modified_millis: nowMs }); try { wss.broadcastToUser(row.user, { type: "updated", id, entity: "activity", deleted_at: nextDeletedAt }); } catch (e) { console.warn("⚠️ WS broadcast activity toggle soft fallito:", e.message); } console.log( `✅ [TOGGLE SOFT /activities/${id}] user=${row.user} soft_deleted=${!!nextDeletedAt} durata=${Date.now() - start}ms` ); res.json({ ok: true, id, user: row.user, deleted_at: nextDeletedAt, soft_deleted: !!nextDeletedAt, restored: !nextDeletedAt, duration_ms: Date.now() - start }); } catch (err) { console.error("❌ Errore POST /activities/:id/toggle-soft:", err); res.status(500).json({ ok: false, error: err.message }); } }); // ----------------------------------------------------- // DELETE /activities/:id // HARD DELETE attività per ID // - Sposta originale in public/del_activities//original// // - Rimuove record da activities // - Registra ID in deleted_hard_activities // ----------------------------------------------------- router.delete('/:id', async (req, res) => { const id = req.params.id; const loggedUser = req.user?.name; const start = Date.now(); try { if (!loggedUser) { return res.status(401).json({ ok: false, error: "Unauthorized" }); } console.log(`🗑️ [HARD DELETE /activities/${id}] richiesto da=${loggedUser}`); let row; if (loggedUser === "Admin") { row = await db('activities') .where({ id }) .first(); } else { row = await db('activities') .where({ id, user: loggedUser }) .first(); } if (!row) { return res.status(404).json({ ok: false, error: "Activity not found or not allowed", id }); } const deletedAt = new Date().toISOString(); // 1. Sposta file originale attività const originalSrc = getOriginalActivityPath(row); const originalDst = getDeletedActivityPath(row); const originalMoved = await moveFileSafe(originalSrc, originalDst); // 2. Elimina record da activities await db('activities') .where({ id }) .del(); // 3. Registra hard delete await db('deleted_hard_activities') .insert({ id, user: row.user, deleted_at: deletedAt }) .onConflict('id') .merge(); // 4. WS realtime try { wss.broadcastToUser(row.user, { type: "removed", id, entity: "activity" }); } catch (e) { console.warn("⚠️ WS broadcast activity hard delete fallito:", e.message); } console.log( `✅ [HARD DELETE /activities/${id}] user=${row.user} durata=${Date.now() - start}ms` ); res.json({ ok: true, id, user: row.user, deleted_at: deletedAt, hard_deleted: true, files: { original_src: originalSrc, original_dst: originalDst, original_moved: originalMoved }, duration_ms: Date.now() - start }); } catch (err) { console.error("❌ Errore HARD DELETE /activities/:id:", err); res.status(500).json({ ok: false, error: err.message }); } }); // ----------------------------------------------------- // GET /activities/:id // Deve stare DOPO deleted_hard, changes, byIds, initDB, // sync_done, toggle-soft e delete. // ----------------------------------------------------- router.get('/:id', async (req, res) => { const id = req.params.id; const loggedUser = req.user?.name; const start = Date.now(); console.log( `📤 [GET /activities/${id}] Richiesta da=${loggedUser} at=${new Date().toISOString()}` ); try { let rows; if (loggedUser === "Admin") { rows = await db('activities').where({ id }); } else { rows = await db('activities') .where({ id }) .whereIn('user', [loggedUser, "Common"]); } console.log( `📤 [GET /activities/${id}] Risposta: ${rows.length} attività durata=${Date.now() - start}ms` ); if (!rows.length) return res.json([]); res.json(rows.map(normalizeActivity)); } catch (err) { console.error("❌ Errore GET /activities/:id:", err); res.status(500).json({ error: err.message }); } }); module.exports = router;