732 lines
19 KiB
JavaScript
732 lines
19 KiB
JavaScript
// 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/<user>/original/<cartella>/
|
|
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/<user>/original/<cartella>/<attività>
|
|
// - 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;
|