Bladeren bron

Add auth middleware and geo/GPX utilities

k4be 7 uur geleden
bovenliggende
commit
0d2b4b4141

+ 26 - 0
gpx-vis-backend/src/middleware/auth.js

@@ -0,0 +1,26 @@
+const jwt = require('jsonwebtoken');
+let config;
+try { config = require('../../config'); } catch(e) { config = { jwt: { secret: 'dev' } }; }
+
+function requireAuth(req, res, next) {
+  const auth = req.headers.authorization;
+  if (!auth || !auth.startsWith('Bearer ')) {
+    return res.status(401).json({ error: 'Not authenticated' });
+  }
+  const token = auth.slice(7);
+  try {
+    req.user = jwt.verify(token, config.jwt.secret);
+    next();
+  } catch (e) {
+    res.status(401).json({ error: 'Invalid or expired token' });
+  }
+}
+
+function requireAdmin(req, res, next) {
+  requireAuth(req, res, () => {
+    if (!req.user.isAdmin) return res.status(403).json({ error: 'Admin required' });
+    next();
+  });
+}
+
+module.exports = { requireAuth, requireAdmin };

+ 25 - 0
gpx-vis-backend/src/utils/geo.js

@@ -0,0 +1,25 @@
+const R = 6371000; // Earth radius in meters
+
+function toRad(deg) { return deg * Math.PI / 180; }
+
+function haversineDistance(lat1, lon1, lat2, lon2) {
+  const dLat = toRad(lat2 - lat1);
+  const dLon = toRad(lon2 - lon1);
+  const a = Math.sin(dLat/2)**2 + Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLon/2)**2;
+  return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
+}
+
+function bearing(lat1, lon1, lat2, lon2) {
+  const dLon = toRad(lon2 - lon1);
+  const y = Math.sin(dLon) * Math.cos(toRad(lat2));
+  const x = Math.cos(toRad(lat1)) * Math.sin(toRad(lat2)) - Math.sin(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.cos(dLon);
+  return (Math.atan2(y, x) * 180 / Math.PI + 360) % 360;
+}
+
+function bearingDiff(b1, b2) {
+  let diff = Math.abs(b1 - b2);
+  if (diff > 180) diff = 360 - diff;
+  return diff;
+}
+
+module.exports = { haversineDistance, bearing, bearingDiff };

+ 106 - 0
gpx-vis-backend/src/utils/gpx-processor.js

@@ -0,0 +1,106 @@
+const xml2js = require('xml2js');
+const { haversineDistance, bearing, bearingDiff } = require('./geo');
+
+async function parseGPX(xmlString) {
+  const parser = new xml2js.Parser({ explicitArray: true, mergeAttrs: false });
+  return parser.parseStringPromise(xmlString);
+}
+
+function extractPoints(gpx) {
+  const trks = gpx.gpx.trk || [];
+  const name = (trks[0]?.name?.[0]) || (gpx.gpx.metadata?.[0]?.name?.[0]) || 'Track';
+  const segments = [];
+  for (const trk of trks) {
+    for (const seg of (trk.trkseg || [])) {
+      const pts = [];
+      for (const pt of (seg.trkpt || [])) {
+        const lat = parseFloat(pt.$.lat);
+        const lon = parseFloat(pt.$.lon);
+        const ele = pt.ele ? parseFloat(pt.ele[0]) : null;
+        const timeStr = pt.time ? pt.time[0] : null;
+        const time = timeStr ? new Date(timeStr) : null;
+        if (!isNaN(lat) && !isNaN(lon)) {
+          pts.push({ lat, lon, elevation: ele, time });
+        }
+      }
+      if (pts.length > 0) segments.push(pts);
+    }
+  }
+  return { name, segments };
+}
+
+function compressSegment(points) {
+  if (points.length === 0) return [];
+  if (points.length === 1) return [points[0]];
+
+  const kept = [points[0]];
+
+  for (let i = 1; i < points.length; i++) {
+    const last = kept[kept.length - 1];
+    const curr = points[i];
+    const next = points[i + 1] || null;
+
+    // Distance check
+    const dist = haversineDistance(last.lat, last.lon, curr.lat, curr.lon);
+    if (dist < 2) continue;
+
+    // Time check
+    if (last.time && curr.time) {
+      const timeDiff = (curr.time.getTime() - last.time.getTime()) / 1000;
+      if (timeDiff < 30) {
+        // Check for sharp turn
+        if (next) {
+          const b1 = bearing(last.lat, last.lon, curr.lat, curr.lon);
+          const b2 = bearing(curr.lat, curr.lon, next.lat, next.lon);
+          if (bearingDiff(b1, b2) <= 30) continue;
+        } else {
+          continue;
+        }
+      }
+    }
+
+    kept.push(curr);
+  }
+
+  // Always ensure last point is included
+  const lastOrig = points[points.length - 1];
+  if (kept[kept.length - 1] !== lastOrig) {
+    const dist = haversineDistance(kept[kept.length-1].lat, kept[kept.length-1].lon, lastOrig.lat, lastOrig.lon);
+    if (dist >= 2) kept.push(lastOrig);
+  }
+
+  return kept;
+}
+
+function calcDistance(points) {
+  let total = 0;
+  for (let i = 1; i < points.length; i++) {
+    total += haversineDistance(points[i-1].lat, points[i-1].lon, points[i].lat, points[i].lon);
+  }
+  return total;
+}
+
+async function parseAndCompress(xmlString) {
+  const gpx = await parseGPX(xmlString);
+  const { name, segments } = extractPoints(gpx);
+
+  let trackDate = null;
+  let totalDistance = 0;
+  let sequence = 0;
+  const resultSegments = [];
+
+  for (let segIdx = 0; segIdx < segments.length; segIdx++) {
+    const seg = segments[segIdx];
+    if (!trackDate && seg[0]?.time) trackDate = seg[0].time;
+    const compressed = compressSegment(seg);
+    totalDistance += calcDistance(compressed);
+    const points = compressed.map(p => ({ ...p, sequence: sequence++, segmentId: segIdx }));
+    resultSegments.push({ points });
+  }
+
+  const pointCount = resultSegments.reduce((s, seg) => s + seg.points.length, 0);
+
+  return { segments: resultSegments, trackName: name, trackDate, totalDistance, pointCount };
+}
+
+module.exports = { parseAndCompress };

+ 13 - 0
gpx-vis-backend/src/utils/shortcode.js

@@ -0,0 +1,13 @@
+const CONSONANTS = 'bcdfghjklmnprstvwxyz';
+const VOWELS = 'aeiou';
+
+function generateCode() {
+  let code = '';
+  for (let i = 0; i < 5; i++) {
+    code += CONSONANTS[Math.floor(Math.random() * CONSONANTS.length)];
+    code += VOWELS[Math.floor(Math.random() * VOWELS.length)];
+  }
+  return code;
+}
+
+module.exports = { generateCode };