mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-19 13:21:46 +00:00
v3.0.21 Bug Fixes (#998)
* fix(journey): remove photo upload count limit and surface upload errors (#997) Removes the arbitrary 10-file cap on journey entry photo uploads and 20-file cap on gallery uploads. MulterErrors now return proper 4xx responses instead of 500, and the client surfaces the server error message via toast rather than silently trapping the user in the post editor overlay. * fix(planner): remove correct assignment when place assigned to same day multiple times When a place was assigned to the same day more than once, the "Remove from day" button in PlaceInspector always deleted the first assignment (Array.find on place.id) instead of the currently selected one. Now prefers selectedAssignmentId when available. Fixes #1005 * fix(map): enable 3D terrain for Mapbox outdoors style in trip planner wantsTerrain() only matched satellite styles, so the outdoors-v12 style was flat in the planner despite showing correct 3D terrain in the settings preview. Added outdoors-v12 to the allowlist; marker drift is already handled by syncMarkerAltitudes(). Fixes #1002 * fix(maps): send Referer header on Google API calls when APP_URL is set Supports HTTP referrer restrictions on GCP API keys. Documents the restriction types and photo troubleshooting steps in the wiki.
This commit is contained in:
@@ -8,13 +8,15 @@ export function isStandardFamily(style: string): boolean {
|
||||
return style === 'mapbox://styles/mapbox/standard' || style === 'mapbox://styles/mapbox/standard-satellite'
|
||||
}
|
||||
|
||||
// Terrain is only genuinely useful for the satellite imagery styles — on
|
||||
// clean flat styles like streets/light/dark it nudges route lines onto
|
||||
// the DEM while our HTML markers stay at Z=0, which causes the visible
|
||||
// offset when the map is pitched. Restrict terrain to satellite.
|
||||
// Terrain is only genuinely useful for styles that benefit from elevation
|
||||
// data. On flat vector styles (streets/light/dark) it nudges route lines
|
||||
// onto the DEM while HTML markers stay at Z=0, causing a visible drift
|
||||
// when the map is pitched. Satellite and Outdoors are the intended styles
|
||||
// for terrain; markers are re-pinned by syncMarkerAltitudes().
|
||||
export function wantsTerrain(style: string): boolean {
|
||||
return style === 'mapbox://styles/mapbox/satellite-v9'
|
||||
|| style === 'mapbox://styles/mapbox/satellite-streets-v12'
|
||||
|| style === 'mapbox://styles/mapbox/outdoors-v12'
|
||||
}
|
||||
|
||||
// 3D can be added to every style now — the standard family has it built-in
|
||||
|
||||
@@ -169,7 +169,10 @@ export default function PlaceInspector({
|
||||
|
||||
const category = categories?.find(c => c.id === place.category_id)
|
||||
const dayAssignments = selectedDayId ? (assignments[String(selectedDayId)] || []) : []
|
||||
const assignmentInDay = selectedDayId ? dayAssignments.find(a => a.place?.id === place.id) : null
|
||||
const assignmentInDay = selectedDayId
|
||||
? ((selectedAssignmentId ? dayAssignments.find(a => a.id === selectedAssignmentId) : null)
|
||||
?? dayAssignments.find(a => a.place?.id === place.id))
|
||||
: null
|
||||
|
||||
const openingHours = googleDetails?.opening_hours || null
|
||||
const openNow = googleDetails?.open_now ?? null
|
||||
|
||||
@@ -1674,6 +1674,7 @@ const ar: Record<string, string | { name: string; category: string }[]> = {
|
||||
'journey.settings.failedToDelete': 'فشل في الحذف',
|
||||
'journey.entries.deleteTitle': 'حذف الإدخال',
|
||||
'journey.photosUploaded': 'تم رفع {count} صورة',
|
||||
'journey.photosUploadFailed': 'فشل رفع بعض الصور',
|
||||
'journey.photosAdded': 'تمت إضافة {count} صورة',
|
||||
'journey.picker.tripPeriod': 'فترة الرحلة',
|
||||
'journey.picker.dateRange': 'نطاق التاريخ',
|
||||
@@ -1705,6 +1706,7 @@ const ar: Record<string, string | { name: string; category: string }[]> = {
|
||||
|
||||
// Journey Entry Editor
|
||||
'journey.editor.discardChangesConfirm': 'لديك تغييرات غير محفوظة. هل تريد تجاهلها؟',
|
||||
'journey.editor.uploadFailed': 'فشل رفع الصور',
|
||||
'journey.editor.uploadPhotos': 'رفع صور',
|
||||
'journey.editor.uploading': '...جارٍ الرفع',
|
||||
'journey.editor.fromGallery': 'من المعرض',
|
||||
|
||||
@@ -2077,6 +2077,7 @@ const br: Record<string, string | { name: string; category: string }[]> = {
|
||||
'journey.synced.places': 'lugares',
|
||||
'journey.synced.synced': 'sincronizado',
|
||||
'journey.editor.discardChangesConfirm': 'Você tem alterações não salvas. Descartá-las?',
|
||||
'journey.editor.uploadFailed': 'Falha ao enviar fotos',
|
||||
'journey.editor.uploadPhotos': 'Enviar fotos',
|
||||
'journey.editor.uploading': 'Enviando...',
|
||||
'journey.editor.fromGallery': 'Da galeria',
|
||||
@@ -2169,6 +2170,7 @@ const br: Record<string, string | { name: string; category: string }[]> = {
|
||||
'journey.settings.failedToDelete': 'Falha ao excluir',
|
||||
'journey.entries.deleteTitle': 'Excluir entrada',
|
||||
'journey.photosUploaded': '{count} fotos enviadas',
|
||||
'journey.photosUploadFailed': 'Algumas fotos não foram enviadas',
|
||||
'journey.photosAdded': '{count} fotos adicionadas',
|
||||
'journey.public.notFound': 'Não encontrado',
|
||||
'journey.public.notFoundMessage': 'Esta jornada não existe ou o link expirou.',
|
||||
|
||||
@@ -2082,6 +2082,7 @@ const cs: Record<string, string | { name: string; category: string }[]> = {
|
||||
'journey.synced.places': 'místa',
|
||||
'journey.synced.synced': 'synchronizováno',
|
||||
'journey.editor.discardChangesConfirm': 'Máte neuložené změny. Zahodit?',
|
||||
'journey.editor.uploadFailed': 'Nahrávání fotek selhalo',
|
||||
'journey.editor.uploadPhotos': 'Nahrát fotky',
|
||||
'journey.editor.uploading': 'Nahrávání...',
|
||||
'journey.editor.fromGallery': 'Z galerie',
|
||||
@@ -2174,6 +2175,7 @@ const cs: Record<string, string | { name: string; category: string }[]> = {
|
||||
'journey.settings.failedToDelete': 'Smazání se nezdařilo',
|
||||
'journey.entries.deleteTitle': 'Smazat záznam',
|
||||
'journey.photosUploaded': '{count} fotografií nahráno',
|
||||
'journey.photosUploadFailed': 'Některé fotky se nepodařilo nahrát',
|
||||
'journey.photosAdded': '{count} fotografií přidáno',
|
||||
'journey.public.notFound': 'Nenalezeno',
|
||||
'journey.public.notFoundMessage': 'Tento cestovní deník neexistuje nebo odkaz vypršel.',
|
||||
|
||||
@@ -2085,6 +2085,7 @@ const de: Record<string, string | { name: string; category: string }[]> = {
|
||||
'journey.synced.places': 'Orte',
|
||||
'journey.synced.synced': 'synchronisiert',
|
||||
'journey.editor.discardChangesConfirm': 'Du hast ungespeicherte Änderungen. Verwerfen?',
|
||||
'journey.editor.uploadFailed': 'Foto-Upload fehlgeschlagen',
|
||||
'journey.editor.uploadPhotos': 'Fotos hochladen',
|
||||
'journey.editor.uploading': 'Hochladen...',
|
||||
'journey.editor.fromGallery': 'Aus Galerie',
|
||||
@@ -2181,6 +2182,7 @@ const de: Record<string, string | { name: string; category: string }[]> = {
|
||||
'journey.settings.failedToDelete': 'Löschen fehlgeschlagen',
|
||||
'journey.entries.deleteTitle': 'Eintrag löschen',
|
||||
'journey.photosUploaded': '{count} Fotos hochgeladen',
|
||||
'journey.photosUploadFailed': 'Einige Fotos konnten nicht hochgeladen werden',
|
||||
'journey.photosAdded': '{count} Fotos hinzugefügt',
|
||||
'journey.public.notFound': 'Nicht gefunden',
|
||||
'journey.public.notFoundMessage': 'Diese Journey existiert nicht oder der Link ist abgelaufen.',
|
||||
|
||||
@@ -2111,6 +2111,7 @@ const en: Record<string, string | { name: string; category: string }[]> = {
|
||||
|
||||
// Journey Entry Editor
|
||||
'journey.editor.discardChangesConfirm': 'You have unsaved changes. Discard them?',
|
||||
'journey.editor.uploadFailed': 'Photo upload failed',
|
||||
'journey.editor.uploadPhotos': 'Upload photos',
|
||||
'journey.editor.uploading': 'Uploading...',
|
||||
'journey.editor.fromGallery': 'From Gallery',
|
||||
@@ -2219,6 +2220,7 @@ const en: Record<string, string | { name: string; category: string }[]> = {
|
||||
'journey.settings.failedToDelete': 'Failed to delete',
|
||||
'journey.entries.deleteTitle': 'Delete Entry',
|
||||
'journey.photosUploaded': '{count} photos uploaded',
|
||||
'journey.photosUploadFailed': 'Some photos failed to upload',
|
||||
'journey.photosAdded': '{count} photos added',
|
||||
|
||||
// Journey — Public Page
|
||||
|
||||
@@ -2084,6 +2084,7 @@ const es: Record<string, string> = {
|
||||
'journey.synced.places': 'lugares',
|
||||
'journey.synced.synced': 'sincronizado',
|
||||
'journey.editor.discardChangesConfirm': 'Tienes cambios sin guardar. ¿Descartarlos?',
|
||||
'journey.editor.uploadFailed': 'Error al subir fotos',
|
||||
'journey.editor.uploadPhotos': 'Subir fotos',
|
||||
'journey.editor.uploading': 'Subiendo...',
|
||||
'journey.editor.fromGallery': 'Desde galería',
|
||||
@@ -2176,6 +2177,7 @@ const es: Record<string, string> = {
|
||||
'journey.settings.failedToDelete': 'Error al eliminar',
|
||||
'journey.entries.deleteTitle': 'Eliminar entrada',
|
||||
'journey.photosUploaded': '{count} fotos subidas',
|
||||
'journey.photosUploadFailed': 'Algunas fotos no se pudieron subir',
|
||||
'journey.photosAdded': '{count} fotos añadidas',
|
||||
'journey.public.notFound': 'No encontrado',
|
||||
'journey.public.notFoundMessage': 'Esta travesía no existe o el enlace ha expirado.',
|
||||
|
||||
@@ -2078,6 +2078,7 @@ const fr: Record<string, string> = {
|
||||
'journey.synced.places': 'lieux',
|
||||
'journey.synced.synced': 'synchronisé',
|
||||
'journey.editor.discardChangesConfirm': 'Vous avez des modifications non enregistrées. Les ignorer ?',
|
||||
'journey.editor.uploadFailed': 'Échec du téléversement des photos',
|
||||
'journey.editor.uploadPhotos': 'Téléverser des photos',
|
||||
'journey.editor.uploading': 'Envoi...',
|
||||
'journey.editor.fromGallery': 'Depuis la galerie',
|
||||
@@ -2170,6 +2171,7 @@ const fr: Record<string, string> = {
|
||||
'journey.settings.failedToDelete': 'Échec de la suppression',
|
||||
'journey.entries.deleteTitle': "Supprimer l'entrée",
|
||||
'journey.photosUploaded': '{count} photos téléversées',
|
||||
'journey.photosUploadFailed': "Certaines photos n'ont pas pu être téléversées",
|
||||
'journey.photosAdded': '{count} photos ajoutées',
|
||||
'journey.public.notFound': 'Introuvable',
|
||||
'journey.public.notFoundMessage': 'Ce journal n\'existe pas ou le lien a expiré.',
|
||||
|
||||
@@ -2079,6 +2079,7 @@ const hu: Record<string, string | { name: string; category: string }[]> = {
|
||||
'journey.synced.places': 'helyszín',
|
||||
'journey.synced.synced': 'szinkronizálva',
|
||||
'journey.editor.discardChangesConfirm': 'Mentetlen módosításaid vannak. Elveted?',
|
||||
'journey.editor.uploadFailed': 'A fotók feltöltése sikertelen',
|
||||
'journey.editor.uploadPhotos': 'Fotók feltöltése',
|
||||
'journey.editor.uploading': 'Feltöltés...',
|
||||
'journey.editor.fromGallery': 'Galériából',
|
||||
@@ -2171,6 +2172,7 @@ const hu: Record<string, string | { name: string; category: string }[]> = {
|
||||
'journey.settings.failedToDelete': 'Törlés sikertelen',
|
||||
'journey.entries.deleteTitle': 'Bejegyzés törlése',
|
||||
'journey.photosUploaded': '{count} fotó feltöltve',
|
||||
'journey.photosUploadFailed': 'Néhány fotót nem sikerült feltölteni',
|
||||
'journey.photosAdded': '{count} fotó hozzáadva',
|
||||
'journey.public.notFound': 'Nem található',
|
||||
'journey.public.notFoundMessage': 'Ez az útinapló nem létezik vagy a link lejárt.',
|
||||
|
||||
@@ -2094,6 +2094,7 @@ const id: Record<string, string | { name: string; category: string }[]> = {
|
||||
|
||||
// Journey Entry Editor
|
||||
'journey.editor.discardChangesConfirm': 'Anda memiliki perubahan yang belum disimpan. Buang?',
|
||||
'journey.editor.uploadFailed': 'Gagal mengunggah foto',
|
||||
'journey.editor.uploadPhotos': 'Unggah foto',
|
||||
'journey.editor.uploading': 'Mengunggah...',
|
||||
'journey.editor.fromGallery': 'Dari Galeri',
|
||||
@@ -2198,6 +2199,7 @@ const id: Record<string, string | { name: string; category: string }[]> = {
|
||||
'journey.settings.failedToDelete': 'Gagal menghapus',
|
||||
'journey.entries.deleteTitle': 'Hapus Entri',
|
||||
'journey.photosUploaded': '{count} foto diunggah',
|
||||
'journey.photosUploadFailed': 'Beberapa foto gagal diunggah',
|
||||
'journey.photosAdded': '{count} foto ditambahkan',
|
||||
|
||||
// Journey — Public Page
|
||||
|
||||
@@ -2079,6 +2079,7 @@ const it: Record<string, string | { name: string; category: string }[]> = {
|
||||
'journey.synced.places': 'luoghi',
|
||||
'journey.synced.synced': 'sincronizzato',
|
||||
'journey.editor.discardChangesConfirm': 'Hai modifiche non salvate. Vuoi scartarle?',
|
||||
'journey.editor.uploadFailed': 'Caricamento foto non riuscito',
|
||||
'journey.editor.uploadPhotos': 'Carica foto',
|
||||
'journey.editor.uploading': 'Caricamento...',
|
||||
'journey.editor.fromGallery': 'Dalla galleria',
|
||||
@@ -2171,6 +2172,7 @@ const it: Record<string, string | { name: string; category: string }[]> = {
|
||||
'journey.settings.failedToDelete': 'Eliminazione non riuscita',
|
||||
'journey.entries.deleteTitle': 'Elimina voce',
|
||||
'journey.photosUploaded': '{count} foto caricate',
|
||||
'journey.photosUploadFailed': 'Alcune foto non sono state caricate',
|
||||
'journey.photosAdded': '{count} foto aggiunte',
|
||||
'journey.public.notFound': 'Non trovato',
|
||||
'journey.public.notFoundMessage': 'Questo diario non esiste o il link è scaduto.',
|
||||
|
||||
@@ -2078,6 +2078,7 @@ const nl: Record<string, string> = {
|
||||
'journey.synced.places': 'plaatsen',
|
||||
'journey.synced.synced': 'gesynchroniseerd',
|
||||
'journey.editor.discardChangesConfirm': 'Je hebt niet-opgeslagen wijzigingen. Verwerpen?',
|
||||
'journey.editor.uploadFailed': 'Foto uploaden mislukt',
|
||||
'journey.editor.uploadPhotos': 'Foto\'s uploaden',
|
||||
'journey.editor.uploading': 'Uploaden...',
|
||||
'journey.editor.fromGallery': 'Uit galerij',
|
||||
@@ -2170,6 +2171,7 @@ const nl: Record<string, string> = {
|
||||
'journey.settings.failedToDelete': 'Verwijderen mislukt',
|
||||
'journey.entries.deleteTitle': 'Vermelding verwijderen',
|
||||
'journey.photosUploaded': "{count} foto's geüpload",
|
||||
'journey.photosUploadFailed': "Sommige foto's konden niet worden geüpload",
|
||||
'journey.photosAdded': "{count} foto's toegevoegd",
|
||||
'journey.public.notFound': 'Niet gevonden',
|
||||
'journey.public.notFoundMessage': 'Dit reisverslag bestaat niet of de link is verlopen.',
|
||||
|
||||
@@ -2071,6 +2071,7 @@ const pl: Record<string, string | { name: string; category: string }[]> = {
|
||||
'journey.synced.places': 'miejsca',
|
||||
'journey.synced.synced': 'zsynchronizowane',
|
||||
'journey.editor.discardChangesConfirm': 'Masz niezapisane zmiany. Odrzucić?',
|
||||
'journey.editor.uploadFailed': 'Przesyłanie zdjęć nie powiodło się',
|
||||
'journey.editor.uploadPhotos': 'Prześlij zdjęcia',
|
||||
'journey.editor.uploading': 'Przesyłanie...',
|
||||
'journey.editor.fromGallery': 'Z galerii',
|
||||
@@ -2163,6 +2164,7 @@ const pl: Record<string, string | { name: string; category: string }[]> = {
|
||||
'journey.settings.failedToDelete': 'Nie udało się usunąć',
|
||||
'journey.entries.deleteTitle': 'Usuń wpis',
|
||||
'journey.photosUploaded': '{count} zdjęć przesłanych',
|
||||
'journey.photosUploadFailed': 'Nie udało się przesłać niektórych zdjęć',
|
||||
'journey.photosAdded': '{count} zdjęć dodanych',
|
||||
'journey.public.notFound': 'Nie znaleziono',
|
||||
'journey.public.notFoundMessage': 'Ten dziennik podróży nie istnieje lub link wygasł.',
|
||||
|
||||
@@ -2078,6 +2078,7 @@ const ru: Record<string, string> = {
|
||||
'journey.synced.places': 'мест',
|
||||
'journey.synced.synced': 'синхронизировано',
|
||||
'journey.editor.discardChangesConfirm': 'У вас есть несохранённые изменения. Отменить?',
|
||||
'journey.editor.uploadFailed': 'Не удалось загрузить фото',
|
||||
'journey.editor.uploadPhotos': 'Загрузить фото',
|
||||
'journey.editor.uploading': 'Загрузка...',
|
||||
'journey.editor.fromGallery': 'Из галереи',
|
||||
@@ -2170,6 +2171,7 @@ const ru: Record<string, string> = {
|
||||
'journey.settings.failedToDelete': 'Не удалось удалить',
|
||||
'journey.entries.deleteTitle': 'Удалить запись',
|
||||
'journey.photosUploaded': '{count} фото загружено',
|
||||
'journey.photosUploadFailed': 'Некоторые фото не удалось загрузить',
|
||||
'journey.photosAdded': '{count} фото добавлено',
|
||||
'journey.public.notFound': 'Не найдено',
|
||||
'journey.public.notFoundMessage': 'Это путешествие не существует или ссылка устарела.',
|
||||
|
||||
@@ -2078,6 +2078,7 @@ const zh: Record<string, string> = {
|
||||
'journey.synced.places': '个地点',
|
||||
'journey.synced.synced': '已同步',
|
||||
'journey.editor.discardChangesConfirm': '您有未保存的更改。要放弃吗?',
|
||||
'journey.editor.uploadFailed': '照片上传失败',
|
||||
'journey.editor.uploadPhotos': '上传照片',
|
||||
'journey.editor.uploading': '上传中...',
|
||||
'journey.editor.fromGallery': '从相册',
|
||||
@@ -2170,6 +2171,7 @@ const zh: Record<string, string> = {
|
||||
'journey.settings.failedToDelete': '删除失败',
|
||||
'journey.entries.deleteTitle': '删除条目',
|
||||
'journey.photosUploaded': '{count} 张照片已上传',
|
||||
'journey.photosUploadFailed': '部分照片上传失败',
|
||||
'journey.photosAdded': '{count} 张照片已添加',
|
||||
'journey.public.notFound': '未找到',
|
||||
'journey.public.notFoundMessage': '此旅程不存在或链接已过期。',
|
||||
|
||||
@@ -2036,6 +2036,7 @@ const zhTw: Record<string, string> = {
|
||||
'journey.synced.places': '個地點',
|
||||
'journey.synced.synced': '已同步',
|
||||
'journey.editor.discardChangesConfirm': '您有未儲存的變更。要放棄嗎?',
|
||||
'journey.editor.uploadFailed': '照片上傳失敗',
|
||||
'journey.editor.uploadPhotos': '上傳照片',
|
||||
'journey.editor.uploading': '上傳中...',
|
||||
'journey.editor.fromGallery': '從相簿',
|
||||
@@ -2128,6 +2129,7 @@ const zhTw: Record<string, string> = {
|
||||
'journey.settings.failedToDelete': '刪除失敗',
|
||||
'journey.entries.deleteTitle': '刪除條目',
|
||||
'journey.photosUploaded': '{count} 張照片已上傳',
|
||||
'journey.photosUploadFailed': '部分照片上傳失敗',
|
||||
'journey.photosAdded': '{count} 張照片已新增',
|
||||
'journey.public.notFound': '未找到',
|
||||
'journey.public.notFoundMessage': '此旅程不存在或連結已過期。',
|
||||
|
||||
@@ -30,6 +30,7 @@ import MobileEntryView from '../components/Journey/MobileEntryView'
|
||||
import { useIsMobile } from '../hooks/useIsMobile'
|
||||
import type { JourneyEntry, JourneyPhoto, GalleryPhoto, JourneyTrip, JourneyDetail } from '../store/journeyStore'
|
||||
import { computeJourneyLifecycle } from '../utils/journeyLifecycle'
|
||||
import { getApiErrorMessage } from '../types'
|
||||
|
||||
const GRADIENTS = [
|
||||
'linear-gradient(135deg, #0F172A 0%, #6366F1 45%, #EC4899 100%)',
|
||||
@@ -1034,8 +1035,8 @@ function GalleryView({ entries, gallery, journeyId, userId, trips, onPhotoClick,
|
||||
await journeyApi.uploadGalleryPhotos(journeyId, formData)
|
||||
toast.success(t('journey.photosUploaded', { count: files.length }))
|
||||
onRefresh()
|
||||
} catch {
|
||||
toast.error(t('journey.settings.coverFailed'))
|
||||
} catch (err) {
|
||||
toast.error(getApiErrorMessage(err, t('journey.photosUploadFailed')))
|
||||
} finally {
|
||||
setGalleryUploading(false)
|
||||
}
|
||||
@@ -2175,6 +2176,7 @@ function EntryEditor({ entry, journeyId, tripDates, galleryPhotos, onClose, onSa
|
||||
onDone: () => void
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
const toast = useToast()
|
||||
const isMobile = useIsMobile()
|
||||
const [title, setTitle] = useState(entry.title || '')
|
||||
const [story, setStory] = useState(entry.story || '')
|
||||
@@ -2248,7 +2250,11 @@ function EntryEditor({ entry, journeyId, tripDates, galleryPhotos, onClose, onSa
|
||||
if (pendingFiles.length > 0 && entryId) {
|
||||
const formData = new FormData()
|
||||
for (const f of pendingFiles) formData.append('photos', f)
|
||||
try {
|
||||
await onUploadPhotos(entryId, formData)
|
||||
} catch (err) {
|
||||
toast.error(getApiErrorMessage(err, t('journey.editor.uploadFailed')))
|
||||
}
|
||||
}
|
||||
// link gallery photos that were picked before save
|
||||
if (pendingLinkIds.length > 0 && entryId) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import cookieParser from 'cookie-parser';
|
||||
import path from 'node:path';
|
||||
import fs from 'node:fs';
|
||||
|
||||
import multer from 'multer';
|
||||
import { logDebug, logWarn, logError } from './services/auditLog';
|
||||
import { enforceGlobalMfaPolicy } from './middleware/mfaPolicy';
|
||||
import { authenticate, verifyJwtAndLoadUser } from './middleware/auth';
|
||||
@@ -507,6 +508,10 @@ export function createApp(): express.Application {
|
||||
} else {
|
||||
console.error('Unhandled error:', err);
|
||||
}
|
||||
if (err instanceof multer.MulterError) {
|
||||
const status = err.code === 'LIMIT_FILE_SIZE' ? 413 : 400;
|
||||
return res.status(status).json({ error: err.message });
|
||||
}
|
||||
const status = err.statusCode || err.status || 500;
|
||||
// Expose the message for client errors (4xx); keep 'Internal server error' for 5xx.
|
||||
const message = status < 500 ? err.message : 'Internal server error';
|
||||
|
||||
@@ -98,7 +98,7 @@ router.delete('/entries/:entryId', authenticate, (req: Request, res: Response) =
|
||||
|
||||
// ── Photos (prefix /photos and /entries — before /:id) ───────────────────
|
||||
|
||||
router.post('/entries/:entryId/photos', authenticate, upload.array('photos', 10), async (req: Request, res: Response) => {
|
||||
router.post('/entries/:entryId/photos', authenticate, upload.array('photos'), async (req: Request, res: Response) => {
|
||||
const authReq = req as AuthRequest;
|
||||
const files = req.files as Express.Multer.File[];
|
||||
if (!files?.length) return res.status(400).json({ error: 'No files uploaded' });
|
||||
@@ -201,7 +201,7 @@ router.delete('/photos/:photoId', authenticate, async (req: Request, res: Respon
|
||||
// ── Gallery (prefix /:id/gallery — before /:id) ──────────────────────────
|
||||
|
||||
// Upload photos directly to the journey gallery (no entry association)
|
||||
router.post('/:id/gallery/photos', authenticate, upload.array('photos', 20), async (req: Request, res: Response) => {
|
||||
router.post('/:id/gallery/photos', authenticate, upload.array('photos'), async (req: Request, res: Response) => {
|
||||
const authReq = req as AuthRequest;
|
||||
const files = req.files as Express.Multer.File[];
|
||||
if (!files?.length) return res.status(400).json({ error: 'No files uploaded' });
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { db } from '../db/database';
|
||||
import { decrypt_api_key } from './apiKeyCrypto';
|
||||
import { checkSsrf } from '../utils/ssrfGuard';
|
||||
import { getAppUrl } from './notifications';
|
||||
|
||||
// ── Google API call counter ───────────────────────────────────────────────────
|
||||
|
||||
@@ -12,7 +13,11 @@ export function resetGoogleApiCallCount(): void { googleApiCallCount = 0; }
|
||||
function googleFetch(endpoint: string, label: string, init?: RequestInit): Promise<Response> {
|
||||
googleApiCallCount++;
|
||||
console.debug(`[Google API] #${googleApiCallCount} ${label} → ${endpoint}`);
|
||||
return fetch(endpoint, init);
|
||||
const referer = process.env.APP_URL ? getAppUrl() : undefined;
|
||||
return fetch(endpoint, {
|
||||
...init,
|
||||
headers: { ...(referer ? { Referer: referer } : {}), ...(init?.headers as Record<string, string> ?? {}) },
|
||||
});
|
||||
}
|
||||
|
||||
// ── Interfaces ───────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -21,6 +21,8 @@ Type in the search box at the top of the form. After 2 or more characters, with
|
||||
|
||||
When a key is present, the autocomplete uses the Google Places API, which can return ratings, opening hours, photos, and phone numbers from Google's database.
|
||||
|
||||
> **API key restrictions:** TREK calls the Google Places API from the server, not the browser. If you apply **HTTP referrers** restrictions to your key in Google Cloud Console, you must also set `APP_URL` in your environment — TREK sends it as the `Referer` header on every outbound Google API request. Without it, Google will reject all server-side calls with `REQUEST_DENIED`. For server-side deployments, **IP address** restrictions are simpler and require no extra configuration. See [Troubleshooting](Troubleshooting) if photos are missing after adding a key.
|
||||
|
||||
### Without a Google Maps API key
|
||||
|
||||
TREK falls back to OpenStreetMap (Nominatim) automatically — no API key needed. A notice appears above the search box explaining that OpenStreetMap is in use and that photos, ratings, and opening hours are unavailable. Results include name, address, and coordinates.
|
||||
|
||||
@@ -223,6 +223,45 @@ If `ALLOWED_ORIGINS` is not set, TREK allows all origins (development default).
|
||||
|
||||
---
|
||||
|
||||
## Place photos not loading / place thumbnail shows default map pin (Google Maps API key configured)
|
||||
|
||||
**Cause:** When a Google Maps API key is set, TREK fetches photo references and image bytes from the Google Places API on the server side. If the server-side call is rejected or returns no photos, the `/place-photo/:id` endpoint returns 404 and the place falls back to the default map-pin thumbnail. The most common causes are:
|
||||
|
||||
1. **HTTP referrer restriction on the API key.** Google Cloud Console lets you restrict a key to specific HTTP referrers. Because TREK calls Google from the server (not the browser), it sends a `Referer` header derived from `APP_URL`. If `APP_URL` is not set, the fallback is `http://localhost:<PORT>`, which will not match any domain whitelist in GCP.
|
||||
|
||||
2. **Wrong key restriction type.** API keys restricted by **HTTP referrers** are designed for browser-side JavaScript. For a self-hosted server application, use **IP address** restrictions instead — add the public IP of your TREK server and no `APP_URL` configuration is needed.
|
||||
|
||||
3. **Places API (New) not enabled.** The key must have **Places API (New)** enabled in Google Cloud Console under APIs & Services → Enabled APIs. Enabling only the legacy Places API is not sufficient.
|
||||
|
||||
4. **Billing not set up.** Google requires a billing account to be linked to the project even within the free tier. Without it, photo and details requests return `REQUEST_DENIED`.
|
||||
|
||||
**Fix for HTTP referrer restriction:**
|
||||
|
||||
Set `APP_URL` to the public URL of your instance and add that URL (or its domain with a wildcard, e.g. `https://trek.example.com/*`) to the allowed referrers in GCP:
|
||||
|
||||
```yaml
|
||||
environment:
|
||||
- APP_URL=https://trek.example.com
|
||||
```
|
||||
|
||||
**Fix for wrong restriction type:**
|
||||
|
||||
Switch the key's "Application restrictions" from **HTTP referrers** to **IP addresses** in Google Cloud Console, and add your server's public IP. No `APP_URL` change needed.
|
||||
|
||||
**Verifying the issue:**
|
||||
|
||||
Run the following curl command using your key to check whether Google returns photo references:
|
||||
|
||||
```bash
|
||||
curl "https://places.googleapis.com/v1/places/<PLACE_ID>" \
|
||||
-H "X-Goog-Api-Key: YOUR_API_KEY" \
|
||||
-H "X-Goog-FieldMask: photos"
|
||||
```
|
||||
|
||||
If the response is `{}` or `{"error": {...}}`, the key or its restrictions are blocking the request. If it returns a `photos` array, the key is valid and the issue is elsewhere.
|
||||
|
||||
---
|
||||
|
||||
## MCP OAuth flow does not initiate / "Connect" redirects but authentication never starts
|
||||
|
||||
**Cause:** TREK builds the OAuth 2.1 redirect URI from `APP_URL`. If `APP_URL` is not set, the authorization URL is constructed from a localhost fallback that external clients (Claude.ai, Claude Desktop) cannot reach, so the OAuth handshake never completes.
|
||||
|
||||
Reference in New Issue
Block a user