Spaces:
Sleeping
Sleeping
File size: 3,458 Bytes
57a889c | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 | import { Injectable } from '@nestjs/common';
import type {
MapsSearchResult,
MapsAutocompleteResult,
MapsPlaceDetailsResult,
MapsPlacePhotoResult,
MapsReverseResult,
MapsResolveUrlResult,
} from '@trek/shared';
import { DatabaseService } from '../database/database.service';
import {
searchPlaces,
autocompletePlaces,
getPlaceDetails,
getPlaceDetailsExpanded,
getPlacePhoto,
reverseGeocode,
resolveGoogleMapsUrl,
searchOverpassPois,
} from '../../services/mapsService';
import { serveFilePath } from '../../services/placePhotoCache';
type LocationBias = { low: { lat: number; lng: number }; high: { lat: number; lng: number } };
/**
* Thin Nest wrapper around the existing maps service. All geocoding, the
* provider fan-out (Nominatim/Overpass/Google) and — importantly — the SSRF
* guard live in mapsService and are reused unchanged, so behaviour and the
* outbound-URL protection are identical.
*
* The per-endpoint kill-switches are settings reads the legacy route does
* inline; they're encapsulated here as `*Disabled()` helpers over the same
* `app_settings` rows.
*/
@Injectable()
export class MapsService {
constructor(private readonly database: DatabaseService) {}
private isSettingDisabled(key: string): boolean {
const row = this.database.get<{ value: string }>(
'SELECT value FROM app_settings WHERE key = ?',
key,
);
return row?.value === 'false';
}
autocompleteDisabled(): boolean {
return this.isSettingDisabled('places_autocomplete_enabled');
}
detailsDisabled(): boolean {
return this.isSettingDisabled('places_details_enabled');
}
photosDisabled(): boolean {
return this.isSettingDisabled('places_photos_enabled');
}
search(userId: number, query: string, lang?: string, locationBias?: { lat: number; lng: number; radius?: number }): Promise<MapsSearchResult> {
return searchPlaces(userId, query, lang, locationBias) as Promise<MapsSearchResult>;
}
autocomplete(userId: number, input: string, lang?: string, locationBias?: LocationBias): Promise<MapsAutocompleteResult> {
return autocompletePlaces(userId, input, lang, locationBias) as Promise<MapsAutocompleteResult>;
}
details(userId: number, placeId: string, lang?: string): Promise<MapsPlaceDetailsResult> {
return getPlaceDetails(userId, placeId, lang) as Promise<MapsPlaceDetailsResult>;
}
detailsExpanded(userId: number, placeId: string, lang: string | undefined, refresh: boolean): Promise<MapsPlaceDetailsResult> {
return getPlaceDetailsExpanded(userId, placeId, lang, refresh) as Promise<MapsPlaceDetailsResult>;
}
photo(userId: number, placeId: string, lat: number, lng: number, name?: string): Promise<MapsPlacePhotoResult> {
return getPlacePhoto(userId, placeId, lat, lng, name) as Promise<MapsPlacePhotoResult>;
}
photoBytesPath(placeId: string): string | null {
return serveFilePath(placeId);
}
reverse(lat: string, lng: string, lang?: string): Promise<MapsReverseResult> {
return reverseGeocode(lat, lng, lang) as Promise<MapsReverseResult>;
}
resolveUrl(url: string): Promise<MapsResolveUrlResult> {
return resolveGoogleMapsUrl(url) as Promise<MapsResolveUrlResult>;
}
// OSM-only POI search by category within a viewport bbox (never calls Google).
pois(category: string, bbox: { south: number; west: number; north: number; east: number }) {
return searchOverpassPois(category, bbox);
}
}
|