fixed missing endpoint GET /routes/:id/pois/:id and added json style fileupload
This commit is contained in:
+3
-1
@@ -4,6 +4,7 @@ import { fileURLToPath } from 'node:url';
|
||||
|
||||
const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const resolveFromRoot = (value, fallback) => path.resolve(projectRoot, value || fallback);
|
||||
const maxUploadBytes = Number.parseInt(process.env.MAX_UPLOAD_MB || '50', 10) * 1024 * 1024;
|
||||
|
||||
export const config = Object.freeze({
|
||||
projectRoot,
|
||||
@@ -11,7 +12,8 @@ export const config = Object.freeze({
|
||||
port: Number.parseInt(process.env.PORT || '47145', 10),
|
||||
dataDir: resolveFromRoot(process.env.DATA_DIR, 'data'),
|
||||
storageDir: resolveFromRoot(process.env.STORAGE_DIR, 'storage'),
|
||||
maxUploadBytes: Number.parseInt(process.env.MAX_UPLOAD_MB || '50', 10) * 1024 * 1024,
|
||||
maxUploadBytes,
|
||||
maxJsonBodyBytes: Math.ceil(maxUploadBytes * 4 / 3) + 1024 * 1024,
|
||||
defaultRouteRadiusKm: Number.parseFloat(process.env.DEFAULT_ROUTE_RADIUS_KM || '25'),
|
||||
defaultPoiTriggerMeters: Number.parseFloat(process.env.DEFAULT_POI_TRIGGER_METERS || '80')
|
||||
});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import crypto from 'node:crypto';
|
||||
import multer from 'multer';
|
||||
@@ -28,3 +29,87 @@ export const upload = multer({
|
||||
limits: { fileSize: config.maxUploadBytes, files: 25 },
|
||||
fileFilter
|
||||
});
|
||||
|
||||
|
||||
function decodeBase64(value, fieldName) {
|
||||
if (typeof value !== 'string' || !value.trim()) {
|
||||
throw new HttpError(400, `${fieldName}.base64 muss eine nichtleere Base64-Zeichenkette sein.`);
|
||||
}
|
||||
|
||||
const compact = value.replace(/\s+/g, '');
|
||||
if (!/^[A-Za-z0-9+/]*={0,2}$/.test(compact) || compact.length % 4 === 1) {
|
||||
throw new HttpError(400, `${fieldName}.base64 enthält keine gültigen Base64-Daten.`);
|
||||
}
|
||||
|
||||
const buffer = Buffer.from(compact, 'base64');
|
||||
const canonicalInput = compact.replace(/=+$/, '');
|
||||
const canonicalOutput = buffer.toString('base64').replace(/=+$/, '');
|
||||
if (canonicalInput !== canonicalOutput) {
|
||||
throw new HttpError(400, `${fieldName}.base64 enthält keine gültigen Base64-Daten.`);
|
||||
}
|
||||
if (buffer.length > config.maxUploadBytes) {
|
||||
throw new HttpError(413, `Die Datei überschreitet das Limit von ${Math.floor(config.maxUploadBytes / 1024 / 1024)} MB.`);
|
||||
}
|
||||
return buffer;
|
||||
}
|
||||
|
||||
function jsonFileDescriptor(value, fieldName) {
|
||||
if (value == null) return null;
|
||||
if (typeof value !== 'object' || Array.isArray(value)) {
|
||||
throw new HttpError(400, `${fieldName} muss ein JSON-Objekt sein.`);
|
||||
}
|
||||
|
||||
const originalname = String(value.filename ?? '').trim();
|
||||
if (!originalname) throw new HttpError(400, `${fieldName}.filename ist erforderlich.`);
|
||||
|
||||
let mimetype = String(value.contentType ?? value.mimeType ?? '').trim().toLowerCase();
|
||||
let base64 = value.base64;
|
||||
|
||||
if (value.dataUrl != null) {
|
||||
if (typeof value.dataUrl !== 'string') {
|
||||
throw new HttpError(400, `${fieldName}.dataUrl muss eine Zeichenkette sein.`);
|
||||
}
|
||||
const match = /^data:([^;,]+);base64,(.*)$/s.exec(value.dataUrl.trim());
|
||||
if (!match) {
|
||||
throw new HttpError(400, `${fieldName}.dataUrl muss eine Base64-Data-URL sein.`);
|
||||
}
|
||||
if (!mimetype) mimetype = match[1].trim().toLowerCase();
|
||||
base64 = match[2];
|
||||
}
|
||||
|
||||
if (!mimetype) throw new HttpError(400, `${fieldName}.contentType ist erforderlich.`);
|
||||
return { originalname, mimetype, buffer: decodeBase64(base64, fieldName) };
|
||||
}
|
||||
|
||||
export function jsonUpload(fieldName) {
|
||||
return function parseJsonUpload(req, res, next) {
|
||||
try {
|
||||
if (req.file || req.body?.[fieldName] == null) return next();
|
||||
|
||||
const descriptor = jsonFileDescriptor(req.body[fieldName], fieldName);
|
||||
if (!descriptor) return next();
|
||||
|
||||
const destination = path.join(config.storageDir, 'uploads');
|
||||
fs.mkdirSync(destination, { recursive: true });
|
||||
const safe = descriptor.originalname.replace(/[^a-zA-Z0-9._-]/g, '_');
|
||||
const filename = `${Date.now()}-${crypto.randomUUID()}-${safe}`;
|
||||
const filePath = path.join(destination, filename);
|
||||
fs.writeFileSync(filePath, descriptor.buffer, { flag: 'wx' });
|
||||
|
||||
req.file = {
|
||||
fieldname: fieldName,
|
||||
originalname: descriptor.originalname,
|
||||
encoding: '7bit',
|
||||
mimetype: descriptor.mimetype,
|
||||
destination,
|
||||
filename,
|
||||
path: filePath,
|
||||
size: descriptor.buffer.length
|
||||
};
|
||||
delete req.body[fieldName];
|
||||
next();
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
+17
-10
@@ -1,5 +1,5 @@
|
||||
import { Router } from 'express';
|
||||
import { upload } from '../middleware/upload.js';
|
||||
import { jsonUpload, upload } from '../middleware/upload.js';
|
||||
import { config } from '../config.js';
|
||||
import {
|
||||
listRoutes, getRoute, getRouteGpxFile, createRoute, updateRoute, appendRoute,
|
||||
@@ -64,8 +64,8 @@ export function createApiRouter(db) {
|
||||
res.json({ pois: listPois(db, Number(req.params.id)) });
|
||||
});
|
||||
|
||||
api.get('/pois/:id', (req, res) => {
|
||||
res.json(getPoi(db, Number(req.params.id)));
|
||||
api.get('/routes/:routeId/pois/:poiId', (req, res) => {
|
||||
res.json(getPoi(db, Number(req.params.routeId), Number(req.params.poiId)));
|
||||
});
|
||||
|
||||
api.post('/routes', upload.single('gpx'), (req, res) => {
|
||||
@@ -83,11 +83,18 @@ export function createApiRouter(db) {
|
||||
|
||||
api.post('/routes/:id/pois', upload.none(), (req, res) => {
|
||||
const poi = createPoi(db, Number(req.params.id), req.body);
|
||||
res.status(201).location(`/api/pois/${poi.id}`).json(poi);
|
||||
res.status(201)
|
||||
.location(`/api/routes/${req.params.id}/pois/${poi.id}`)
|
||||
.json(poi);
|
||||
});
|
||||
|
||||
api.put('/pois/:id', upload.none(), (req, res) => {
|
||||
res.json(updatePoi(db, Number(req.params.id), req.body));
|
||||
api.put('/routes/:routeId/pois/:poiId', upload.none(), (req, res) => {
|
||||
res.json(updatePoi(
|
||||
db,
|
||||
Number(req.params.routeId),
|
||||
Number(req.params.poiId),
|
||||
req.body
|
||||
));
|
||||
});
|
||||
|
||||
api.get('/routes/:routeId/pois/:poiId/pictures', (req, res) => {
|
||||
@@ -105,7 +112,7 @@ export function createApiRouter(db) {
|
||||
sendFileResource(res, getPictureFile(db, routeId, poiId, pictureId));
|
||||
});
|
||||
|
||||
api.post('/routes/:routeId/pois/:poiId/pictures', upload.single('picture'), (req, res) => {
|
||||
api.post('/routes/:routeId/pois/:poiId/pictures', upload.single('picture'), jsonUpload('picture'), (req, res) => {
|
||||
const routeId = Number(req.params.routeId);
|
||||
const poiId = Number(req.params.poiId);
|
||||
const picture = createPicture(db, routeId, poiId, req.body, req.file);
|
||||
@@ -114,7 +121,7 @@ export function createApiRouter(db) {
|
||||
.json(picture);
|
||||
});
|
||||
|
||||
api.put('/routes/:routeId/pois/:poiId/pictures/:pictureId', upload.single('picture'), (req, res) => {
|
||||
api.put('/routes/:routeId/pois/:poiId/pictures/:pictureId', upload.single('picture'), jsonUpload('picture'), (req, res) => {
|
||||
res.json(updatePicture(
|
||||
db,
|
||||
Number(req.params.routeId),
|
||||
@@ -144,7 +151,7 @@ export function createApiRouter(db) {
|
||||
sendFileResource(res, getAudioFile(db, routeId, poiId));
|
||||
});
|
||||
|
||||
api.post('/routes/:routeId/pois/:poiId/audio', upload.single('audio'), (req, res) => {
|
||||
api.post('/routes/:routeId/pois/:poiId/audio', upload.single('audio'), jsonUpload('audio'), (req, res) => {
|
||||
const routeId = Number(req.params.routeId);
|
||||
const poiId = Number(req.params.poiId);
|
||||
const audio = createAudio(db, routeId, poiId, req.file);
|
||||
@@ -153,7 +160,7 @@ export function createApiRouter(db) {
|
||||
.json(audio);
|
||||
});
|
||||
|
||||
api.put('/routes/:routeId/pois/:poiId/audio', upload.single('audio'), (req, res) => {
|
||||
api.put('/routes/:routeId/pois/:poiId/audio', upload.single('audio'), jsonUpload('audio'), (req, res) => {
|
||||
res.json(updateAudio(db, Number(req.params.routeId), Number(req.params.poiId), req.file));
|
||||
});
|
||||
|
||||
|
||||
@@ -151,10 +151,11 @@ export function listPois(db, routeId) {
|
||||
}));
|
||||
}
|
||||
|
||||
export function getPoi(db, poiId) {
|
||||
const row = db.prepare(`SELECT p.route_id AS routeId FROM pois p JOIN routes r ON r.id = p.route_id WHERE p.id = ? AND r.status = 'active'`).get(poiId);
|
||||
export function getPoi(db, routeId, poiId) {
|
||||
const row = db.prepare(`SELECT p.id FROM pois p JOIN routes r ON r.id = p.route_id
|
||||
WHERE p.id = ? AND p.route_id = ? AND r.status = 'active'`).get(poiId, routeId);
|
||||
if (!row) throw new HttpError(404, 'POI nicht gefunden.');
|
||||
return listPois(db, row.routeId).find(poi => poi.id === Number(poiId));
|
||||
return listPois(db, routeId).find(poi => poi.id === Number(poiId));
|
||||
}
|
||||
|
||||
export function createPoi(db, routeId, fields) {
|
||||
@@ -171,8 +172,9 @@ export function createPoi(db, routeId, fields) {
|
||||
return listPois(db, routeId).find(poi => poi.id === Number(result.lastInsertRowid));
|
||||
}
|
||||
|
||||
export function updatePoi(db, poiId, fields) {
|
||||
const existing = db.prepare(`SELECT p.*, r.status FROM pois p JOIN routes r ON r.id = p.route_id WHERE p.id = ?`).get(poiId);
|
||||
export function updatePoi(db, routeId, poiId, fields) {
|
||||
const existing = db.prepare(`SELECT p.*, r.status FROM pois p JOIN routes r ON r.id = p.route_id
|
||||
WHERE p.id = ? AND p.route_id = ?`).get(poiId, routeId);
|
||||
if (!existing || existing.status !== 'active') throw new HttpError(404, 'POI nicht gefunden.');
|
||||
const lat = fields.lat == null ? existing.lat : Number.parseFloat(fields.lat);
|
||||
const lon = fields.lon == null ? existing.lon : Number.parseFloat(fields.lon);
|
||||
|
||||
Reference in New Issue
Block a user