Introduced a request-scoped `Map` (`authCache`) in `app/api/map/route.ts` to memoize `isAuthenticated` calls. The `getMapData` loop iterates over all albums across all locations. Albums within the same subpage slug trigger repeated `isAuthenticated` checks, which execute expensive HMAC cryptographic operations, causing performance degradation for large datasets. Eliminates redundant cryptographic operations during map data fetching, reducing CPU utilization and response times for the Map API. Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
84 lines
2.7 KiB
TypeScript
84 lines
2.7 KiB
TypeScript
/**
|
|
* Map data API — returns location-level markers for the map view.
|
|
* Each marker represents a unique city/country with averaged GPS coords.
|
|
* Cover asset IDs are encrypted for security.
|
|
*/
|
|
|
|
import { NextRequest, NextResponse } from 'next/server';
|
|
import { cookies } from 'next/headers';
|
|
import { getConfig } from '@/lib/config';
|
|
import { imageUrl } from '@/lib/urls';
|
|
import { isAuthenticated } from '@/lib/auth';
|
|
import { getMapData } from '@/lib/mapService';
|
|
import { checkRateLimit, getClientIp } from '@/lib/rate-limit';
|
|
|
|
export const dynamic = 'force-dynamic';
|
|
|
|
export async function GET(request: NextRequest) {
|
|
// Rate limit: 120 requests/minute per IP (map data is cached client-side; this guards against abuse)
|
|
const ip = getClientIp(request);
|
|
const rl = checkRateLimit(ip, 120);
|
|
if (!rl.success) {
|
|
return NextResponse.json(
|
|
{ error: 'Too many requests' },
|
|
{
|
|
status: 429,
|
|
headers: {
|
|
'Retry-After': String(Math.ceil((rl.resetAt - Date.now()) / 1000)),
|
|
'X-RateLimit-Limit': '120',
|
|
'X-RateLimit-Remaining': '0',
|
|
},
|
|
},
|
|
);
|
|
}
|
|
|
|
const config = getConfig();
|
|
|
|
if (!config.map) {
|
|
return NextResponse.json({ error: 'Map is not enabled' }, { status: 404 });
|
|
}
|
|
|
|
const cookieStore = await cookies();
|
|
const getCookie = (name: string) => cookieStore.get(name)?.value;
|
|
|
|
const locations = await getMapData();
|
|
|
|
// Filter locations and albums based on auth
|
|
const authCache = new Map<string, boolean>();
|
|
const publicLocations = locations
|
|
.map((loc) => {
|
|
// Only keep albums the user is allowed to see
|
|
const allowedAlbums = loc.albums.filter((a) => {
|
|
if (!a.subpageSlug) return true; // Standalone albums are public
|
|
if (authCache.has(a.subpageSlug)) return authCache.get(a.subpageSlug)!;
|
|
const result = isAuthenticated(a.subpageSlug, getCookie);
|
|
authCache.set(a.subpageSlug, result);
|
|
return result;
|
|
});
|
|
|
|
if (allowedAlbums.length === 0) return null;
|
|
|
|
return {
|
|
city: loc.city,
|
|
country: loc.country,
|
|
lat: loc.lat,
|
|
lng: loc.lng,
|
|
photoCount: loc.photoCount, // Note: This count might be slightly off if some photos are in hidden albums
|
|
coverUrl: imageUrl(loc.coverAssetId, 'thumbnail'),
|
|
albums: allowedAlbums.map((a) => ({
|
|
name: a.name,
|
|
url: a.subpageSlug ? `/${a.subpageSlug}/${a.slug}` : `/${a.slug}`,
|
|
})),
|
|
};
|
|
})
|
|
.filter((loc) => loc !== null);
|
|
|
|
return NextResponse.json(publicLocations, {
|
|
headers: {
|
|
// Private 5-minute browser cache: reduces re-fetches during a session
|
|
// while still preventing shared/CDN caching of personalised data.
|
|
'Cache-Control': 'private, max-age=300, must-revalidate',
|
|
},
|
|
});
|
|
}
|