fix: back-merge v3.1.4 hotfixes into dev (#1371)

Port the three main-only fixes onto dev's (post-rewrite) architecture:
- fix(backups): prevent recursion when the backup path sits inside the backed-up dir
- fix(share): convert budget items to the viewer's base currency instead of a flat EUR
- fix(files): surface the descriptive server error for unsupported upload types (#1363)

Cherry-picked from 819aa793 on main; the SharedTripPage and useTripPlanner
conflicts were resolved to keep dev's font-scaling and full import set while
taking the fixes' currency conversion and translateApiError wiring.
This commit is contained in:
jubnl
2026-07-01 18:57:16 +02:00
committed by Maurice
parent 3686792ce8
commit 60cbd20327
37 changed files with 276 additions and 18 deletions
+2 -1
View File
@@ -39,7 +39,8 @@ const UPLOAD = {
fileFilter: (_req: unknown, file: Express.Multer.File, cb: (err: Error | null, accept: boolean) => void) => {
const ext = path.extname(file.originalname).toLowerCase();
const reject = () => {
const err: Error & { statusCode?: number } = new Error('File type not allowed');
// i18n key — the client resolves it via t() (see translateApiError).
const err: Error & { statusCode?: number } = new Error('files.uploadErrorType');
err.statusCode = 400;
cb(err, false);
};
+13 -1
View File
@@ -184,9 +184,21 @@ export async function createBackup(): Promise<BackupInfo> {
// Exclude the place-photo and trek-memory caches: both are re-derivable
// (re-fetched on demand, keyed on stable ids) and would otherwise dominate
// backup size. Restores self-heal — the cache dirs are recreated at startup.
//
// Also exclude backups/ and restore-*/: these live under data/, not uploads/,
// but when an install maps data and uploads to the SAME directory (a
// misconfiguration, but a catastrophic one) the glob would otherwise sweep
// every prior backup zip into the new archive — each run embedding all
// previous runs, so size compounds without bound (see issue #1358). Ignoring
// them keeps the backup bounded regardless of how the volumes are mounted.
archive.glob(
'**/*',
{ cwd: uploadsDir, ignore: ['photos/google/**', 'photos/trek/**'], nodir: true, dot: true },
{
cwd: uploadsDir,
ignore: ['photos/google/**', 'photos/trek/**', 'backups/**', 'restore-*/**'],
nodir: true,
dot: true,
},
{ prefix: 'uploads' },
);
}
+17 -1
View File
@@ -2,6 +2,7 @@ import { db, canAccessTrip } from '../db/database';
import crypto from 'crypto';
import { loadTagsByPlaceIds } from './queryHelpers';
import { serveFilePath } from './placePhotoCache';
import { getUserSettings } from './settingsService';
const PLACE_PHOTO_PROXY_PREFIX = '/api/maps/place-photo/';
@@ -219,8 +220,23 @@ export function getSharedTripData(token: string): Record<string, any> | null {
? db.prepare('SELECT m.*, u.username, u.avatar FROM collab_messages m JOIN users u ON m.user_id = u.id WHERE m.trip_id = ? AND m.deleted = 0 ORDER BY m.created_at').all(tripId)
: [];
// Display currency the share owner sees in their Costs view. A public viewer has
// no logged-in user, so the owner's per-user `default_currency` (with the admin
// instance default already merged in by getUserSettings) is embedded in the
// payload and used by the client to convert every expense — otherwise guests
// fall back to the trip's base currency and see the wrong totals (#1361).
// getUserSettings merges admin defaults under the user's own settings, so this
// honours per-user → admin-default; we then fall back to trip currency → EUR.
let baseCurrency = (trip as { currency?: string }).currency || 'EUR';
if (shareRow.created_by != null) {
const ownerDefault = getUserSettings(shareRow.created_by)['default_currency'];
if (typeof ownerDefault === 'string' && ownerDefault.trim()) {
baseCurrency = ownerDefault.trim();
}
}
return {
trip, days, assignments, dayNotes, places, categories, permissions,
trip, baseCurrency, days, assignments, dayNotes, places, categories, permissions,
reservations: permissions.share_bookings ? reservations : [],
accommodations: permissions.share_bookings ? accommodations : [],
packing: permissions.share_packing ? packing : [],