security: implement AUTH_SECRET and secure map/exif API routes

This commit is contained in:
ralksta
2026-03-07 08:18:23 +01:00
parent 2a16a9386f
commit e394fe84a0
14 changed files with 849 additions and 793 deletions

View File

@@ -8,79 +8,80 @@
import { useEffect, useRef, useState } from 'react';
interface MapLocationPublic {
city: string;
country: string;
lat: number;
lng: number;
photoCount: number;
coverUrl: string;
albums: { name: string; url: string }[];
city: string;
country: string;
lat: number;
lng: number;
photoCount: number;
coverUrl: string;
albums: { name: string; url: string }[];
}
export function MapView() {
const containerRef = useRef<HTMLDivElement>(null);
const mapRef = useRef<unknown>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const containerRef = useRef<HTMLDivElement>(null);
const mapRef = useRef<unknown>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
let cancelled = false;
useEffect(() => {
let cancelled = false;
async function init() {
// Dynamically import Leaflet (client-only)
const L = (await import('leaflet')).default;
async function init() {
// Dynamically import Leaflet (client-only)
const L = (await import('leaflet')).default;
if (cancelled || !containerRef.current || mapRef.current) return;
if (cancelled || !containerRef.current || mapRef.current) return;
// Fetch map data
const res = await fetch('/api/map');
if (!res.ok) {
setError('Failed to load map data');
setLoading(false);
return;
}
const locations: MapLocationPublic[] = await res.json();
// Fetch map data
const res = await fetch('/api/map');
if (!res.ok) {
setError('Failed to load map data');
setLoading(false);
return;
}
const locations: MapLocationPublic[] = await res.json();
if (cancelled) return;
if (cancelled) return;
// Initialize map
const map = L.map(containerRef.current, {
zoomControl: true,
attributionControl: true,
});
mapRef.current = map;
// Initialize map
const map = L.map(containerRef.current, {
zoomControl: true,
attributionControl: true,
});
mapRef.current = map;
// Dark-themed tiles (CartoDB Dark Matter)
L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OSM</a> &copy; <a href="https://carto.com/">CARTO</a>',
subdomains: 'abcd',
maxZoom: 19,
}).addTo(map);
// Dark-themed tiles (CartoDB Dark Matter)
L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', {
attribution:
'&copy; <a href="https://www.openstreetmap.org/copyright">OSM</a> &copy; <a href="https://carto.com/">CARTO</a>',
subdomains: 'abcd',
maxZoom: 19,
}).addTo(map);
if (locations.length === 0) {
// No geotagged photos — show world view
map.setView([20, 0], 2);
setLoading(false);
return;
}
if (locations.length === 0) {
// No geotagged photos — show world view
map.setView([20, 0], 2);
setLoading(false);
return;
}
// Create markers with custom HTML icons
const markers: L.Marker[] = [];
// Create markers with custom HTML icons
const markers: L.Marker[] = [];
for (const loc of locations) {
const icon = L.divIcon({
className: '',
html: `<div class="map-marker">${loc.photoCount}</div>`,
iconSize: [32, 32],
iconAnchor: [16, 16],
popupAnchor: [0, -20],
});
for (const loc of locations) {
const icon = L.divIcon({
className: '',
html: `<div class="map-marker">${loc.photoCount}</div>`,
iconSize: [32, 32],
iconAnchor: [16, 16],
popupAnchor: [0, -20],
});
const albumLinks = loc.albums
.map((a) => `<a href="${a.url}" class="map-popup__album-link">${a.name} →</a>`)
.join('');
const albumLinks = loc.albums
.map((a) => `<a href="${a.url}" class="map-popup__album-link">${a.name} →</a>`)
.join('');
const popupHtml = `
const popupHtml = `
<div class="map-popup">
<img src="${loc.coverUrl}" alt="${loc.city}" class="map-popup__cover" loading="lazy" />
<div class="map-popup__body">
@@ -92,61 +93,61 @@ export function MapView() {
</div>
`;
const marker = L.marker([loc.lat, loc.lng], { icon })
.addTo(map)
.bindPopup(popupHtml, { maxWidth: 260, minWidth: 200 });
const marker = L.marker([loc.lat, loc.lng], { icon })
.addTo(map)
.bindPopup(popupHtml, { maxWidth: 260, minWidth: 200 });
markers.push(marker);
}
markers.push(marker);
}
// Fit bounds to show all markers
if (markers.length > 0) {
const group = L.featureGroup(markers);
map.fitBounds(group.getBounds().pad(0.15));
}
// Fit bounds to show all markers
if (markers.length > 0) {
const group = L.featureGroup(markers);
map.fitBounds(group.getBounds().pad(0.15));
}
// Force Leaflet to recalculate tile grid
setTimeout(() => map.invalidateSize(), 100);
// Force Leaflet to recalculate tile grid
setTimeout(() => map.invalidateSize(), 100);
setLoading(false);
}
init().catch((err) => {
console.error('[Map] Init error:', err);
if (!cancelled) {
setError('Failed to initialize map');
setLoading(false);
}
});
return () => {
cancelled = true;
if (mapRef.current) {
(mapRef.current as L.Map).remove();
mapRef.current = null;
}
};
}, []);
if (error) {
return (
<div className="map-container__loading">
<p>{error}</p>
</div>
);
setLoading(false);
}
init().catch((err) => {
console.error('[Map] Init error:', err);
if (!cancelled) {
setError('Failed to initialize map');
setLoading(false);
}
});
return () => {
cancelled = true;
if (mapRef.current) {
(mapRef.current as L.Map).remove();
mapRef.current = null;
}
};
}, []);
if (error) {
return (
<div style={{ position: 'relative' }}>
{loading && (
<div className="map-container__loading" style={{ position: 'absolute', inset: 0, zIndex: 1000 }}>
<p>Loading map…</p>
</div>
)}
<div
ref={containerRef}
className="map-container"
/>
</div>
<div className="map-container__loading">
<p>{error}</p>
</div>
);
}
return (
<div style={{ position: 'relative' }}>
{loading && (
<div
className="map-container__loading"
style={{ position: 'absolute', inset: 0, zIndex: 1000 }}
>
<p>Loading map…</p>
</div>
)}
<div ref={containerRef} className="map-container" />
</div>
);
}