chore: resolve merge conflicts with dev branch

Merge dev into feat/notification-system, keeping all i18n keys from both
branches (notification system keys + reservation price/budget keys).
This commit is contained in:
mauriceboe
2026-04-05 01:43:43 +02:00
21 changed files with 354 additions and 57 deletions

View File

@@ -30,7 +30,7 @@ router.get('/', authenticate, (req: Request, res: Response) => {
router.post('/', authenticate, (req: Request, res: Response) => {
const authReq = req as AuthRequest;
const { tripId } = req.params;
const { title, reservation_time, reservation_end_time, location, confirmation_number, notes, day_id, place_id, assignment_id, status, type, accommodation_id, metadata, create_accommodation } = req.body;
const { title, reservation_time, reservation_end_time, location, confirmation_number, notes, day_id, place_id, assignment_id, status, type, accommodation_id, metadata, create_accommodation, create_budget_entry } = req.body;
const trip = verifyTripAccess(tripId, authReq.user.id);
if (!trip) return res.status(404).json({ error: 'Trip not found' });
@@ -50,6 +50,21 @@ router.post('/', authenticate, (req: Request, res: Response) => {
broadcast(tripId, 'accommodation:created', {}, req.headers['x-socket-id'] as string);
}
// Auto-create budget entry if price was provided
if (create_budget_entry && create_budget_entry.total_price > 0) {
try {
const { createBudgetItem } = require('../services/budgetService');
const budgetItem = createBudgetItem(tripId, {
name: title,
category: create_budget_entry.category || type || 'Other',
total_price: create_budget_entry.total_price,
});
broadcast(tripId, 'budget:created', { item: budgetItem }, req.headers['x-socket-id'] as string);
} catch (err) {
console.error('[reservations] Failed to create budget entry:', err);
}
}
res.status(201).json({ reservation });
broadcast(tripId, 'reservation:created', { reservation }, req.headers['x-socket-id'] as string);
@@ -83,7 +98,7 @@ router.put('/positions', authenticate, (req: Request, res: Response) => {
router.put('/:id', authenticate, (req: Request, res: Response) => {
const authReq = req as AuthRequest;
const { tripId, id } = req.params;
const { title, reservation_time, reservation_end_time, location, confirmation_number, notes, day_id, place_id, assignment_id, status, type, accommodation_id, metadata, create_accommodation } = req.body;
const { title, reservation_time, reservation_end_time, location, confirmation_number, notes, day_id, place_id, assignment_id, status, type, accommodation_id, metadata, create_accommodation, create_budget_entry } = req.body;
const trip = verifyTripAccess(tripId, authReq.user.id);
if (!trip) return res.status(404).json({ error: 'Trip not found' });
@@ -104,6 +119,21 @@ router.put('/:id', authenticate, (req: Request, res: Response) => {
broadcast(tripId, 'accommodation:updated', {}, req.headers['x-socket-id'] as string);
}
// Auto-create budget entry if price was provided
if (create_budget_entry && create_budget_entry.total_price > 0) {
try {
const { createBudgetItem } = require('../services/budgetService');
const budgetItem = createBudgetItem(tripId, {
name: title || current.title,
category: create_budget_entry.category || type || current.type || 'Other',
total_price: create_budget_entry.total_price,
});
broadcast(tripId, 'budget:created', { item: budgetItem }, req.headers['x-socket-id'] as string);
} catch (err) {
console.error('[reservations] Failed to create budget entry:', err);
}
}
res.json({ reservation });
broadcast(tripId, 'reservation:updated', { reservation }, req.headers['x-socket-id'] as string);

View File

@@ -1,6 +1,7 @@
import fetch from 'node-fetch';
import { db } from '../db/database';
import { decrypt_api_key } from './apiKeyCrypto';
import { checkSsrf } from '../utils/ssrfGuard';
// ── Interfaces ───────────────────────────────────────────────────────────────
@@ -474,8 +475,11 @@ export async function reverseGeocode(lat: string, lng: string, lang?: string): P
export async function resolveGoogleMapsUrl(url: string): Promise<{ lat: number; lng: number; name: string | null; address: string | null }> {
let resolvedUrl = url;
// Follow redirects for short URLs (goo.gl, maps.app.goo.gl)
if (url.includes('goo.gl') || url.includes('maps.app')) {
// Follow redirects for short URLs (goo.gl, maps.app.goo.gl) with SSRF protection
const parsed = new URL(url);
if (['goo.gl', 'maps.app.goo.gl'].includes(parsed.hostname)) {
const ssrf = await checkSsrf(url, true);
if (!ssrf.allowed) throw Object.assign(new Error('URL blocked by SSRF check'), { status: 403 });
const redirectRes = await fetch(url, { redirect: 'follow', signal: AbortSignal.timeout(10000) });
resolvedUrl = redirectRes.url;
}