0c4b91707f
CRITICAL fixes: - Fix SQL injection in geoportal search (template literal in $queryRaw) - Preserve enrichment data during GIS re-sync (upsert update explicit fields only) - Fix ePay version race condition (advisory lock in transaction) - Add requireAuth() to compress-pdf and unlock routes (were unauthenticated) - Remove hardcoded Stirling PDF API key (env vars now required) IMPORTANT fixes: - Add admin role check on registratura debug-sequences endpoint - Fix reserved slot race condition with advisory lock in transaction - Use SSO identity in close-guard-dialog instead of hardcoded "Utilizator" - Storage DELETE catches only P2025 (not found), re-throws real errors - Add onDelete: SetNull for GisFeature → GisSyncRun relation - Move portal-only users to PORTAL_ONLY_USERS env var - Add security headers (X-Frame-Options, X-Content-Type-Options, Referrer-Policy) - Add periodic cleanup for eTerra/ePay session caches and progress store - Log warning when ePay dataDocument is missing (expiry fallback) Cleanup: - Delete orphaned rgi-test page (1086 lines, unregistered, inaccessible) - Delete legacy/ folder (5 files, unreferenced from src/) - Remove unused ensureBucketExists() from minio-client.ts Documentation: - Optimize CLAUDE.md: 464 → 197 lines (moved per-module details to docs/) - Create docs/ARCHITECTURE-QUICK.md (80 lines: data flow, deps, env vars) - Create docs/MODULE-MAP.md (140 lines: entry points, API routes, cross-deps) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
153 lines
4.9 KiB
TypeScript
153 lines
4.9 KiB
TypeScript
/**
|
|
* GET /api/geoportal/search?q=...&type=...&limit=...
|
|
*
|
|
* Searches parcels (by cadastral ref, owner) and UATs (by name).
|
|
* Returns centroids in EPSG:4326 (WGS84) for map flyTo.
|
|
*/
|
|
import { NextResponse } from "next/server";
|
|
import { prisma } from "@/core/storage/prisma";
|
|
|
|
export const runtime = "nodejs";
|
|
export const dynamic = "force-dynamic";
|
|
|
|
type SearchResultItem = {
|
|
id: string;
|
|
type: "parcel" | "uat" | "building";
|
|
label: string;
|
|
sublabel?: string;
|
|
coordinates?: [number, number];
|
|
};
|
|
|
|
export async function GET(req: Request) {
|
|
try {
|
|
const url = new URL(req.url);
|
|
const q = url.searchParams.get("q")?.trim() ?? "";
|
|
const typeFilter = url.searchParams.get("type") ?? "";
|
|
const limit = Math.min(parseInt(url.searchParams.get("limit") ?? "20", 10), 50);
|
|
|
|
if (q.length < 2) {
|
|
return NextResponse.json({ results: [] });
|
|
}
|
|
|
|
const results: SearchResultItem[] = [];
|
|
const pattern = `%${q}%`;
|
|
|
|
// Search UATs by name
|
|
if (!typeFilter || typeFilter === "uat") {
|
|
const uats = await prisma.$queryRaw`
|
|
SELECT
|
|
siruta,
|
|
name,
|
|
county,
|
|
ST_X(ST_Centroid(ST_Transform(geom, 4326))) as lng,
|
|
ST_Y(ST_Centroid(ST_Transform(geom, 4326))) as lat
|
|
FROM "GisUat"
|
|
WHERE geom IS NOT NULL
|
|
AND (name ILIKE ${pattern} OR county ILIKE ${pattern})
|
|
ORDER BY name
|
|
LIMIT ${limit}
|
|
` as Array<{ siruta: string; name: string; county: string | null; lng: number; lat: number }>;
|
|
|
|
for (const u of uats) {
|
|
results.push({
|
|
id: `uat-${u.siruta}`,
|
|
type: "uat",
|
|
label: u.name,
|
|
sublabel: u.county ? `Jud. ${u.county}` : undefined,
|
|
coordinates: u.lng && u.lat ? [u.lng, u.lat] : undefined,
|
|
});
|
|
}
|
|
}
|
|
|
|
// Search parcels by cadastral ref or enrichment data
|
|
if (!typeFilter || typeFilter === "parcel") {
|
|
const isNumericish = /^\d/.test(q);
|
|
|
|
if (isNumericish) {
|
|
// Search by cadastral reference
|
|
const parcels = await prisma.$queryRaw`
|
|
SELECT
|
|
id,
|
|
"cadastralRef",
|
|
"areaValue",
|
|
siruta,
|
|
enrichment,
|
|
ST_X(ST_Centroid(ST_Transform(geom, 4326))) as lng,
|
|
ST_Y(ST_Centroid(ST_Transform(geom, 4326))) as lat
|
|
FROM "GisFeature"
|
|
WHERE geom IS NOT NULL
|
|
AND "layerId" LIKE 'TERENURI%'
|
|
AND ("cadastralRef" ILIKE ${pattern}
|
|
OR enrichment::text ILIKE ${`%"NR_CAD":"${q}%`})
|
|
ORDER BY "cadastralRef"
|
|
LIMIT ${limit}
|
|
` as Array<{
|
|
id: string;
|
|
cadastralRef: string | null;
|
|
areaValue: number | null;
|
|
siruta: string;
|
|
enrichment: Record<string, unknown> | null;
|
|
lng: number;
|
|
lat: number;
|
|
}>;
|
|
|
|
for (const p of parcels) {
|
|
const nrCad = (p.enrichment?.NR_CAD as string) ?? p.cadastralRef ?? "?";
|
|
const area = p.areaValue ? `${Math.round(p.areaValue)} mp` : "";
|
|
results.push({
|
|
id: `parcel-${p.id}`,
|
|
type: "parcel",
|
|
label: `Parcela ${nrCad}`,
|
|
sublabel: [area, `SIRUTA ${p.siruta}`].filter(Boolean).join(" | "),
|
|
coordinates: p.lng && p.lat ? [p.lng, p.lat] : undefined,
|
|
});
|
|
}
|
|
} else {
|
|
// Search by owner name in enrichment JSON
|
|
const parcels = await prisma.$queryRaw`
|
|
SELECT
|
|
id,
|
|
"cadastralRef",
|
|
"areaValue",
|
|
siruta,
|
|
enrichment,
|
|
ST_X(ST_Centroid(ST_Transform(geom, 4326))) as lng,
|
|
ST_Y(ST_Centroid(ST_Transform(geom, 4326))) as lat
|
|
FROM "GisFeature"
|
|
WHERE geom IS NOT NULL
|
|
AND "layerId" LIKE 'TERENURI%'
|
|
AND enrichment IS NOT NULL
|
|
AND enrichment::text ILIKE ${pattern}
|
|
ORDER BY "cadastralRef"
|
|
LIMIT ${limit}
|
|
` as Array<{
|
|
id: string;
|
|
cadastralRef: string | null;
|
|
areaValue: number | null;
|
|
siruta: string;
|
|
enrichment: Record<string, unknown> | null;
|
|
lng: number;
|
|
lat: number;
|
|
}>;
|
|
|
|
for (const p of parcels) {
|
|
const nrCad = (p.enrichment?.NR_CAD as string) ?? p.cadastralRef ?? "?";
|
|
const owner = (p.enrichment?.PROPRIETARI as string) ?? "";
|
|
results.push({
|
|
id: `parcel-${p.id}`,
|
|
type: "parcel",
|
|
label: `Parcela ${nrCad}`,
|
|
sublabel: owner.length > 60 ? owner.slice(0, 60) + "..." : owner,
|
|
coordinates: p.lng && p.lat ? [p.lng, p.lat] : undefined,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
return NextResponse.json({ results: results.slice(0, limit) });
|
|
} catch (error) {
|
|
const msg = error instanceof Error ? error.message : "Eroare la cautare";
|
|
return NextResponse.json({ error: msg }, { status: 500 });
|
|
}
|
|
}
|