1072 lines
26 KiB
JavaScript
1072 lines
26 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");
|
|
const { parseFitTrackPoints } = require("../api_v1/scanner/parsers/parseFit");
|
|
const {
|
|
getUserAccess,
|
|
filterReadableSources,
|
|
canWrite,
|
|
} = require("../api_v1/userAccess");
|
|
|
|
// -----------------------------------------------------
|
|
// 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 getActivityReadAccess(req, res) {
|
|
const loggedUser = String(req.user?.name ?? "").trim();
|
|
|
|
if (!loggedUser) {
|
|
res.status(401).json({
|
|
error: "Unauthorized",
|
|
});
|
|
|
|
return null;
|
|
}
|
|
|
|
const access = getUserAccess(loggedUser);
|
|
|
|
if (!access) {
|
|
res.status(403).json({
|
|
error: "Utente non configurato",
|
|
user: loggedUser,
|
|
});
|
|
|
|
return null;
|
|
}
|
|
|
|
return access;
|
|
}
|
|
|
|
function safeJoin(base, ...parts) {
|
|
const resolvedBase = path.resolve(base);
|
|
const resolved = path.resolve(base, ...parts);
|
|
|
|
const isInsideBase =
|
|
resolved === resolvedBase ||
|
|
resolved.startsWith(`${resolvedBase}${path.sep}`);
|
|
|
|
if (!isInsideBase) {
|
|
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 {
|
|
const loggedUser = String(req.user?.name ?? "").trim();
|
|
const targetSource = String(a?.user ?? "").trim();
|
|
|
|
if (!loggedUser) {
|
|
return res.status(401).json({
|
|
ok: false,
|
|
error: "Unauthorized",
|
|
});
|
|
}
|
|
|
|
const access = getUserAccess(loggedUser);
|
|
|
|
if (!access) {
|
|
return res.status(403).json({
|
|
ok: false,
|
|
error: "Utente non configurato",
|
|
user: loggedUser,
|
|
});
|
|
}
|
|
|
|
if (!targetSource) {
|
|
return res.status(400).json({
|
|
ok: false,
|
|
error: "Missing activity user/source",
|
|
});
|
|
}
|
|
|
|
if (!canWrite(loggedUser, targetSource)) {
|
|
console.warn("⛔ Inserimento attività non autorizzato", {
|
|
loggedUser,
|
|
targetSource,
|
|
id: a?.id,
|
|
});
|
|
|
|
return res.status(403).json({
|
|
ok: false,
|
|
error: "Write not allowed",
|
|
targetSource,
|
|
});
|
|
}
|
|
|
|
console.log(
|
|
`🏃 [DB INSERT] Salvo activity id=${a.id} user=${targetSource}`,
|
|
);
|
|
|
|
const now = new Date().toISOString();
|
|
|
|
await db("activities")
|
|
.insert({
|
|
// Mantieni qui tutti i campi attuali
|
|
id: a.id,
|
|
user: targetSource,
|
|
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: now,
|
|
updated_at: now,
|
|
})
|
|
.onConflict("id")
|
|
.merge();
|
|
|
|
wss.broadcastToUser(targetSource, {
|
|
type: "added",
|
|
id: a.id,
|
|
entity: "activity",
|
|
});
|
|
|
|
console.log(
|
|
`✅ [POST /activities] Activity id=${a.id} salvata ` +
|
|
`durata=${Date.now() - start}ms`,
|
|
);
|
|
|
|
return res.json({
|
|
ok: true,
|
|
});
|
|
} catch (err) {
|
|
console.error("❌ Errore POST /activities:", err);
|
|
|
|
return res.status(500).json({
|
|
ok: false,
|
|
error: err.message,
|
|
});
|
|
}
|
|
});
|
|
// -----------------------------------------------------
|
|
// GET /activities/changes
|
|
// Progressive sync
|
|
// -----------------------------------------------------
|
|
router.get("/changes", async (req, res) => {
|
|
const access = getActivityReadAccess(req, res);
|
|
|
|
if (!access) {
|
|
return;
|
|
}
|
|
|
|
const since = req.query.since;
|
|
const start = Date.now();
|
|
|
|
console.log(
|
|
`🔄 [SYNC SINCE] GET /activities/changes ` +
|
|
`user=${access.name} since=${since} ` +
|
|
`at=${new Date().toISOString()}`,
|
|
);
|
|
|
|
console.log("🔐 Sorgenti leggibili:", access.readableSources);
|
|
|
|
if (!since) {
|
|
return res.status(400).json({
|
|
error: "Missing ?since=timestamp",
|
|
});
|
|
}
|
|
|
|
if (access.readableSources.length === 0) {
|
|
return res.json([]);
|
|
}
|
|
|
|
try {
|
|
const rows = await db("activities")
|
|
.whereIn("user", access.readableSources)
|
|
.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`,
|
|
);
|
|
|
|
return res.json(rows.map(normalizeActivity));
|
|
} catch (err) {
|
|
console.error("❌ Errore GET /activities/changes:", err);
|
|
|
|
return res.status(500).json({
|
|
error: err.message,
|
|
});
|
|
}
|
|
});
|
|
|
|
// -----------------------------------------------------
|
|
// GET /activities/byIds
|
|
// -----------------------------------------------------
|
|
router.get("/byIds", async (req, res) => {
|
|
const access = getActivityReadAccess(req, res);
|
|
|
|
if (!access) {
|
|
return;
|
|
}
|
|
|
|
const ids = req.query.id;
|
|
const requestedUsers = req.query.user;
|
|
const start = Date.now();
|
|
|
|
console.log(`📤 [GET /activities/byIds] at=${new Date().toISOString()}`);
|
|
console.log("📤 ids:", ids);
|
|
console.log("📤 requestedUsers:", requestedUsers);
|
|
console.log("🔐 readableSources:", access.readableSources);
|
|
|
|
if (!ids) {
|
|
return res.json([]);
|
|
}
|
|
|
|
const idList = Array.isArray(ids) ? ids : [ids];
|
|
|
|
const allowedSources = filterReadableSources(access.name, requestedUsers);
|
|
|
|
console.log("🔐 allowedSources:", allowedSources);
|
|
|
|
if (allowedSources.length === 0) {
|
|
return res.json([]);
|
|
}
|
|
|
|
try {
|
|
const rows = await db("activities")
|
|
.whereIn("id", idList)
|
|
.whereIn("user", allowedSources);
|
|
|
|
console.log(
|
|
`📤 [GET /activities/byIds] trovate ${rows.length} attività ` +
|
|
`durata=${Date.now() - start}ms`,
|
|
);
|
|
|
|
return res.json(rows.map(normalizeActivity));
|
|
} catch (err) {
|
|
console.error("❌ Errore GET /activities/byIds:", err);
|
|
|
|
return res.status(500).json({
|
|
error: err.message,
|
|
});
|
|
}
|
|
});
|
|
|
|
// -----------------------------------------------------
|
|
// GET /activities
|
|
// Full sync
|
|
// -----------------------------------------------------
|
|
router.get("/", async (req, res) => {
|
|
const access = getActivityReadAccess(req, res);
|
|
|
|
if (!access) {
|
|
return;
|
|
}
|
|
|
|
const start = Date.now();
|
|
|
|
console.log(
|
|
`📤 [FULL SYNC] GET /activities da=${access.name} ` +
|
|
`at=${new Date().toISOString()}`,
|
|
);
|
|
|
|
console.log("🔐 Sorgenti leggibili:", access.readableSources);
|
|
|
|
if (access.readableSources.length === 0) {
|
|
return res.json([]);
|
|
}
|
|
|
|
try {
|
|
const rows = await db("activities")
|
|
.whereIn("user", access.readableSources)
|
|
.orderBy("start_time_millis", "desc");
|
|
|
|
console.log(
|
|
`📤 [FULL SYNC] Risposta: ${rows.length} attività ` +
|
|
`durata=${Date.now() - start}ms`,
|
|
);
|
|
|
|
return res.json(rows.map(normalizeActivity));
|
|
} catch (err) {
|
|
console.error("❌ Errore GET /activities:", err);
|
|
|
|
return 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) => {
|
|
const loggedUser = String(req.user?.name ?? "").trim();
|
|
const start = Date.now();
|
|
|
|
try {
|
|
if (!loggedUser) {
|
|
return res.status(401).json({
|
|
ok: false,
|
|
error: "Unauthorized",
|
|
});
|
|
}
|
|
|
|
const access = getUserAccess(loggedUser);
|
|
|
|
if (!access) {
|
|
return res.status(403).json({
|
|
ok: false,
|
|
error: "Utente non configurato",
|
|
user: loggedUser,
|
|
});
|
|
}
|
|
|
|
const writableSources = access.writableSources;
|
|
|
|
console.log("🧹 [POST /activities/initDB]", {
|
|
requestedBy: loggedUser,
|
|
writableSources,
|
|
});
|
|
|
|
if (writableSources.length === 0) {
|
|
return res.json({
|
|
ok: true,
|
|
scope: "none",
|
|
user: loggedUser,
|
|
writableSources: [],
|
|
deletedActivities: 0,
|
|
deletedHardRecords: 0,
|
|
deletedSyncRecords: 0,
|
|
duration_ms: Date.now() - start,
|
|
message: "Nessuna sorgente scrivibile da svuotare",
|
|
});
|
|
}
|
|
|
|
const result = await db.transaction(async (trx) => {
|
|
const deletedActivities = await trx("activities")
|
|
.whereIn("user", writableSources)
|
|
.del();
|
|
|
|
const deletedHardRecords = await trx("deleted_hard_activities")
|
|
.whereIn("user", writableSources)
|
|
.del();
|
|
|
|
/*
|
|
* sync_done_activities non contiene attualmente la colonna user.
|
|
* Possiamo limitarlo ai device scrivibili usando device_id.
|
|
*/
|
|
const deletedSyncRecords = await trx("sync_done_activities")
|
|
.whereIn("device_id", writableSources)
|
|
.del();
|
|
|
|
return {
|
|
deletedActivities,
|
|
deletedHardRecords,
|
|
deletedSyncRecords,
|
|
};
|
|
});
|
|
|
|
return res.json({
|
|
ok: true,
|
|
scope: "writableSources",
|
|
user: loggedUser,
|
|
writableSources,
|
|
...result,
|
|
duration_ms: Date.now() - start,
|
|
message:
|
|
`DB attività svuotato per le sorgenti scrivibili ` + `di ${loggedUser}`,
|
|
});
|
|
} catch (err) {
|
|
console.error("❌ Errore POST /activities/initDB:", err);
|
|
|
|
return res.status(500).json({
|
|
ok: false,
|
|
error: err.message,
|
|
duration_ms: Date.now() - start,
|
|
});
|
|
}
|
|
});
|
|
|
|
// -----------------------------------------------------
|
|
// POST /activities/initDBuser
|
|
// Reset DB attività per utente specifico
|
|
// Solo Admin
|
|
// -----------------------------------------------------
|
|
router.post("/initDBuser", async (req, res) => {
|
|
try {
|
|
const loggedUser = String(req.user?.name ?? "").trim();
|
|
|
|
if (loggedUser !== "Admin") {
|
|
return res.status(403).json({
|
|
ok: false,
|
|
error: "Forbidden",
|
|
loggedUser,
|
|
});
|
|
}
|
|
|
|
const targetUser = String(
|
|
req.body?.user ||
|
|
req.query?.user ||
|
|
req.body?.name ||
|
|
req.query?.name ||
|
|
"",
|
|
).trim();
|
|
|
|
if (!targetUser) {
|
|
return res.status(400).json({
|
|
ok: false,
|
|
error: "Missing user",
|
|
});
|
|
}
|
|
|
|
console.log(
|
|
`🧹 [POST /activities/initDBuser] ` +
|
|
`Admin=${loggedUser} target=${targetUser}`,
|
|
);
|
|
|
|
const result = await db.transaction(async (trx) => {
|
|
const deletedActivities = await trx("activities")
|
|
.where({ user: targetUser })
|
|
.del();
|
|
|
|
const deletedHardRecords = await trx("deleted_hard_activities")
|
|
.where({ user: targetUser })
|
|
.del();
|
|
|
|
const deletedSyncRecords = await trx("sync_done_activities")
|
|
.where({ device_id: targetUser })
|
|
.del();
|
|
|
|
return {
|
|
deletedActivities,
|
|
deletedHardRecords,
|
|
deletedSyncRecords,
|
|
};
|
|
});
|
|
|
|
return res.json({
|
|
ok: true,
|
|
user: targetUser,
|
|
...result,
|
|
message: `DB attività svuotato per user=${targetUser}`,
|
|
});
|
|
} catch (err) {
|
|
console.error("❌ Errore POST /activities/initDBuser:", err);
|
|
|
|
return 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 access = getActivityReadAccess(req, res);
|
|
|
|
if (!access) {
|
|
return;
|
|
}
|
|
|
|
const since = req.query.since;
|
|
|
|
if (!since) {
|
|
return res.status(400).json({
|
|
error: "Missing ?since=timestamp",
|
|
});
|
|
}
|
|
|
|
if (access.readableSources.length === 0) {
|
|
return res.json({
|
|
deleted: [],
|
|
});
|
|
}
|
|
|
|
try {
|
|
const rows = await db("deleted_hard_activities")
|
|
.whereIn("user", access.readableSources)
|
|
.andWhere("deleted_at", ">", since)
|
|
.orderBy("deleted_at", "asc");
|
|
|
|
return res.json({
|
|
deleted: rows,
|
|
});
|
|
} catch (err) {
|
|
console.error("❌ Errore GET /activities/deleted_hard:", err);
|
|
|
|
return res.status(500).json({
|
|
error: err.message,
|
|
});
|
|
}
|
|
});
|
|
|
|
// -----------------------------------------------------
|
|
// POST /activities/sync_done
|
|
// -----------------------------------------------------
|
|
router.post("/sync_done", async (req, res) => {
|
|
try {
|
|
const loggedUser = String(req.user?.name ?? "").trim();
|
|
const deviceId = String(req.body?.device_id ?? "").trim();
|
|
const sessionId = String(req.body?.session_id ?? "").trim();
|
|
|
|
if (!loggedUser) {
|
|
return res.status(401).json({
|
|
ok: false,
|
|
error: "Unauthorized",
|
|
});
|
|
}
|
|
|
|
if (!getUserAccess(loggedUser)) {
|
|
return res.status(403).json({
|
|
ok: false,
|
|
error: "Utente non configurato",
|
|
user: loggedUser,
|
|
});
|
|
}
|
|
|
|
if (!deviceId) {
|
|
return res.status(400).json({
|
|
ok: false,
|
|
error: "Missing device_id",
|
|
});
|
|
}
|
|
|
|
if (!sessionId) {
|
|
return res.status(400).json({
|
|
ok: false,
|
|
error: "Missing session_id",
|
|
});
|
|
}
|
|
|
|
if (!canWrite(loggedUser, deviceId)) {
|
|
return res.status(403).json({
|
|
ok: false,
|
|
error: "Write not allowed",
|
|
device_id: deviceId,
|
|
});
|
|
}
|
|
|
|
await db("sync_done_activities").insert({
|
|
session_id: sessionId,
|
|
device_id: deviceId,
|
|
last_sync: req.body.last_sync,
|
|
});
|
|
|
|
return res.json({
|
|
ok: true,
|
|
});
|
|
} catch (err) {
|
|
return res.status(500).json({
|
|
ok: false,
|
|
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 = String(req.user?.name ?? "").trim();
|
|
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}`,
|
|
);
|
|
|
|
const row = await db("activities").where({ id }).first();
|
|
|
|
if (!row || !canWrite(loggedUser, row.user)) {
|
|
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 (err) {
|
|
console.warn(
|
|
"⚠️ WS broadcast activity toggle soft fallito:",
|
|
err.message,
|
|
);
|
|
}
|
|
|
|
return res.json({
|
|
ok: true,
|
|
id,
|
|
user: row.user,
|
|
deleted_at: nextDeletedAt,
|
|
soft_deleted: Boolean(nextDeletedAt),
|
|
restored: !nextDeletedAt,
|
|
duration_ms: Date.now() - start,
|
|
});
|
|
} catch (err) {
|
|
console.error("❌ Errore POST /activities/:id/toggle-soft:", err);
|
|
|
|
return 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}`,
|
|
);
|
|
|
|
const row = await db("activities").where({ id }).first();
|
|
|
|
if (!row || !canWrite(loggedUser, row.user)) {
|
|
return res.status(404).json({
|
|
ok: false,
|
|
error: "Activity not found or not allowed",
|
|
id,
|
|
});
|
|
}
|
|
|
|
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/route-points
|
|
// Carica tutti i punti della traccia FIT su richiesta.
|
|
// I punti non vengono salvati nel database.
|
|
// Deve stare PRIMA di GET /activities/:id.
|
|
// -----------------------------------------------------
|
|
router.get("/:id/route-points", async (req, res) => {
|
|
const id = req.params.id;
|
|
|
|
const loggedUser = req.user?.name;
|
|
|
|
//const startedAt = Date.now();
|
|
|
|
if (!loggedUser) {
|
|
return res.status(401).json({
|
|
ok: false,
|
|
error: "Unauthorized",
|
|
});
|
|
}
|
|
|
|
try {
|
|
const access = getUserAccess(loggedUser);
|
|
|
|
if (!access) {
|
|
return res.status(403).json({
|
|
ok: false,
|
|
error: "Utente non configurato",
|
|
user: loggedUser,
|
|
});
|
|
}
|
|
|
|
if (access.readableSources.length === 0) {
|
|
return res.status(404).json({
|
|
ok: false,
|
|
error: "Activity not found or not allowed",
|
|
id,
|
|
});
|
|
}
|
|
|
|
const row = await db("activities")
|
|
.where({ id })
|
|
.whereIn("user", access.readableSources)
|
|
.first();
|
|
|
|
if (!row) {
|
|
return res.status(404).json({
|
|
ok: false,
|
|
error: "Activity not found or not allowed",
|
|
id,
|
|
});
|
|
}
|
|
|
|
const format = String(row.format || row.file_extension || "")
|
|
.toLowerCase()
|
|
.replaceAll(".", "")
|
|
.trim();
|
|
|
|
const fileName = String(row.file_name || "").toLowerCase();
|
|
|
|
const isFit = format === "fit" || fileName.endsWith(".fit");
|
|
|
|
if (!isFit) {
|
|
return res.status(400).json({
|
|
ok: false,
|
|
error: "Full route endpoint currently supports FIT activities only",
|
|
id,
|
|
format,
|
|
});
|
|
}
|
|
|
|
const originalPath = getOriginalActivityPath(row);
|
|
|
|
const exists = await pathExists(originalPath);
|
|
|
|
if (!exists) {
|
|
console.warn(
|
|
"[activity route-points] File FIT non trovato:",
|
|
originalPath,
|
|
);
|
|
|
|
return res.status(404).json({
|
|
ok: false,
|
|
error: "Activity file not found",
|
|
id,
|
|
});
|
|
}
|
|
|
|
const points = await parseFitTrackPoints(originalPath);
|
|
/*
|
|
console.log(
|
|
'[activity route-points] traccia FIT caricata',
|
|
{
|
|
id,
|
|
user:
|
|
row.user,
|
|
requestedBy:
|
|
loggedUser,
|
|
expectedPoints:
|
|
row.point_count,
|
|
loadedPoints:
|
|
points.length,
|
|
durationMs:
|
|
Date.now() -
|
|
startedAt
|
|
}
|
|
);
|
|
*/
|
|
res.set("Cache-Control", "private, no-store");
|
|
|
|
return res.json({
|
|
ok: true,
|
|
activity_id: row.id,
|
|
format: "FIT",
|
|
expected_point_count: Number(row.point_count) || null,
|
|
point_count: points.length,
|
|
points,
|
|
});
|
|
} catch (error) {
|
|
console.error("[activity route-points] Errore:", error);
|
|
|
|
return res.status(500).json({
|
|
ok: false,
|
|
error: "Errore caricamento traccia FIT",
|
|
details: error.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 access = getActivityReadAccess(req, res);
|
|
|
|
if (!access) {
|
|
return;
|
|
}
|
|
|
|
const id = req.params.id;
|
|
const start = Date.now();
|
|
|
|
console.log(
|
|
`📤 [GET /activities/${id}] Richiesta da=${access.name} ` +
|
|
`at=${new Date().toISOString()}`,
|
|
);
|
|
|
|
if (access.readableSources.length === 0) {
|
|
return res.json([]);
|
|
}
|
|
|
|
try {
|
|
const rows = await db("activities")
|
|
.where({ id })
|
|
.whereIn("user", access.readableSources);
|
|
|
|
console.log(
|
|
`📤 [GET /activities/${id}] Risposta: ${rows.length} attività ` +
|
|
`durata=${Date.now() - start}ms`,
|
|
);
|
|
|
|
if (!rows.length) {
|
|
return res.json([]);
|
|
}
|
|
|
|
return res.json(rows.map(normalizeActivity));
|
|
} catch (err) {
|
|
console.error("❌ Errore GET /activities/:id:", err);
|
|
|
|
return res.status(500).json({
|
|
error: err.message,
|
|
});
|
|
}
|
|
});
|
|
|
|
module.exports = router;
|