Files
falah-mobile/src/app/api/halal/places/route.ts
T

158 lines
5.6 KiB
TypeScript
Raw Normal View History

import { NextRequest, NextResponse } from 'next/server'
import { prisma } from '@/lib/prisma'
import { verifyJWT } from '@/lib/auth'
const FREE_DAILY_LIMIT = 20
const CACHE_TTL_MS = 24 * 60 * 60 * 1000
interface OverpassElement {
id: number
lat?: number
lon?: number
center?: { lat: number; lon: number }
tags?: Record<string, string>
}
interface Place {
id: string
lat: number
lon: number
name: string
type: 'mosque' | 'restaurant'
address?: string
}
const OVERPASS_ENDPOINTS = [
'https://overpass-api.de/api/interpreter',
'https://maps.mail.ru/osm/tools/overpass/api/interpreter',
'https://overpass.kumi.systems/api/interpreter',
]
async function queryOverpass(query: string): Promise<OverpassElement[]> {
let lastError: Error | null = null
for (const endpoint of OVERPASS_ENDPOINTS) {
try {
const res = await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Accept': 'application/json',
},
body: `data=${encodeURIComponent(query)}`,
signal: AbortSignal.timeout(15000),
})
if (!res.ok) { lastError = new Error(`Overpass error ${res.status} from ${endpoint}`); continue }
const data = await res.json()
return data.elements || []
} catch (e) {
lastError = e as Error
}
}
throw lastError ?? new Error('All Overpass endpoints failed')
}
function buildMosqueQuery(lat: number, lon: number, radius: number): string {
return `[out:json][timeout:10];(node["amenity"="place_of_worship"]["religion"="muslim"](around:${radius},${lat},${lon});way["amenity"="place_of_worship"]["religion"="muslim"](around:${radius},${lat},${lon}););out center;`
}
function buildRestaurantQuery(lat: number, lon: number, radius: number): string {
return `[out:json][timeout:10];(node["diet:halal"="yes"](around:${radius},${lat},${lon});node["cuisine"~"halal"](around:${radius},${lat},${lon});way["diet:halal"="yes"](around:${radius},${lat},${lon}););out center;`
}
function elementsToPlaces(elements: OverpassElement[], type: 'mosque' | 'restaurant'): Place[] {
return elements
.filter(e => {
const lat = e.lat ?? e.center?.lat
const lon = e.lon ?? e.center?.lon
return lat !== undefined && lon !== undefined
})
.map(e => ({
id: String(e.id),
lat: (e.lat ?? e.center?.lat)!,
lon: (e.lon ?? e.center?.lon)!,
name: e.tags?.name || e.tags?.['name:en'] || (type === 'mosque' ? 'Mosque' : 'Halal Restaurant'),
type,
address: [e.tags?.['addr:street'], e.tags?.['addr:city']].filter(Boolean).join(', ') || undefined,
}))
}
async function getUserFromRequest(req: NextRequest) {
const auth = req.headers.get('authorization')
if (!auth?.startsWith('Bearer ')) return null
const payload = await verifyJWT(auth.slice(7))
if (!payload) return null
return prisma.user.findUnique({ where: { id: payload.id } })
}
export async function GET(req: NextRequest) {
try {
const { searchParams } = new URL(req.url)
const lat = parseFloat(searchParams.get('lat') || '')
const lon = parseFloat(searchParams.get('lng') || '')
const radius = Math.min(parseInt(searchParams.get('radius') || '5000'), 10000)
if (isNaN(lat) || isNaN(lon)) {
return NextResponse.json({ error: 'lat and lng required' }, { status: 400 })
}
const user = await getUserFromRequest(req)
const isPremium = user?.isPremium || user?.isPro || false
// Rate limit free users
if (user && !isPremium) {
const today = new Date()
today.setHours(0, 0, 0, 0)
let usage = await prisma.halalUsage.findUnique({ where: { userId: user.id } })
if (!usage || !usage.periodEnd || usage.periodEnd < today) {
usage = await prisma.halalUsage.upsert({
where: { userId: user.id },
create: { userId: user.id, queriesUsed: 0, queriesLimit: FREE_DAILY_LIMIT, periodEnd: new Date(today.getTime() + 86400000) },
update: { queriesUsed: 0, queriesLimit: FREE_DAILY_LIMIT, periodEnd: new Date(today.getTime() + 86400000) },
})
}
if (usage.queriesUsed >= FREE_DAILY_LIMIT) {
return NextResponse.json({ error: 'daily_limit_reached', limit: FREE_DAILY_LIMIT }, { status: 429 })
}
}
// Check cache
const cacheKey = `${isPremium ? 'premium' : 'free'}:${lat.toFixed(2)}:${lon.toFixed(2)}:${radius}`
const cached = await prisma.halalPlaceCache.findUnique({ where: { cacheKey } })
if (cached && cached.expiresAt > new Date()) {
return NextResponse.json(JSON.parse(cached.data))
}
// Fetch from Overpass
const mosqueElements = await queryOverpass(buildMosqueQuery(lat, lon, radius))
const mosques = elementsToPlaces(mosqueElements, 'mosque')
let restaurants: Place[] = []
if (isPremium) {
const restaurantElements = await queryOverpass(buildRestaurantQuery(lat, lon, radius))
restaurants = elementsToPlaces(restaurantElements, 'restaurant')
}
const result = { mosques, restaurants }
// Cache result
await prisma.halalPlaceCache.upsert({
where: { cacheKey },
create: { cacheKey, data: JSON.stringify(result), expiresAt: new Date(Date.now() + CACHE_TTL_MS) },
update: { data: JSON.stringify(result), expiresAt: new Date(Date.now() + CACHE_TTL_MS) },
})
// Increment usage for free users
if (user && !isPremium) {
await prisma.halalUsage.update({
where: { userId: user.id },
data: { queriesUsed: { increment: 1 } },
})
}
return NextResponse.json(result)
} catch (e) {
console.error('Halal places error:', e)
return NextResponse.json({ error: 'Failed to fetch places' }, { status: 500 })
}
}