From 71df1ee9ece11f0087656839b5087aff9ff633a4 Mon Sep 17 00:00:00 2001 From: Claude VM Date: Tue, 19 May 2026 16:52:47 +0300 Subject: [PATCH] fix(geoportal-v2): surface scope-insufficient instead of silent 404 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The other-session's gis-api investigation found that gis-api is working correctly — full/basic/none scopes all behave per spec. The bug was in our /api/gis/parcela/find proxy: when EVERY candidate returned 403 from gis-api (because the caller's JWT carried no enrichment_scope claim), the proxy swallowed the 403s and returned silent 404. The panel then rendered the "not in central DB" empty state instead of prompting re-login. This was the case for Marius today — his pre-refresh-fix session held a token without the enrichment claim. After the auth self-heal fix (commit 8ff67d1) the next gis-api call would have re-authed correctly, but the panel never gave him that signal because find hid the 403. Fix in two places: 1. /api/gis/parcela/find: - Count 403s seen during candidate iteration - If forbiddenCount > 0 && forbiddenCount === candidates.length, return 403 { error: "scope_insufficient", ... } with a log line [gis-parcela-find] all_candidates_forbidden siruta=X cad=Y N - Otherwise log [gis-parcela-find] no_match (so we never go silent) 2. feature-info-panel: when fetch returns 403, the existing "forbidden" UI was a passive warning. Now it shows an actionable "Re-loghează-te" button that fires signIn("authentik", { callbackUrl: current }) — same path SessionErrorWatcher uses for RefreshAccessTokenError. Reference: gis-api session report 2026-05-19 (Marius forwarded analysis); the gis-api repo is unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/app/api/gis/parcela/find/route.ts | 35 +++++++++++++++++-- .../geoportal/v2/feature-info-panel.tsx | 25 ++++++++++--- 2 files changed, 54 insertions(+), 6 deletions(-) diff --git a/src/app/api/gis/parcela/find/route.ts b/src/app/api/gis/parcela/find/route.ts index f33657e..27dfb80 100644 --- a/src/app/api/gis/parcela/find/route.ts +++ b/src/app/api/gis/parcela/find/route.ts @@ -51,6 +51,7 @@ export async function GET(request: Request) { } let fallback: unknown = null; + let forbiddenCount = 0; for (const c of candidates) { try { const detail = (await gisApi.parcela.get(c.id)) as { @@ -70,8 +71,13 @@ export async function GET(request: Request) { return NextResponse.json(detail); } } catch (e) { - // Skip candidates that 403 (scope) or fail individually. - if (e instanceof GisApiError && e.status === 403) continue; + // Skip candidates that 403 (scope) or fail individually, + // but track 403s so we can distinguish "no data" from + // "all data was scope-blocked" at the route boundary. + if (e instanceof GisApiError && e.status === 403) { + forbiddenCount++; + continue; + } throw e; } } @@ -86,6 +92,31 @@ export async function GET(request: Request) { ); return NextResponse.json(fallback); } + + // No readable candidate AND every read returned 403 → user is + // authenticated but lacks the enrichment scope. Surface this + // explicitly so the panel can prompt re-login instead of showing + // an empty/404 state (the older behaviour was silent 404 — caller + // couldn't tell auth-loss from missing-data). + if (forbiddenCount > 0 && forbiddenCount === candidates.length) { + console.warn( + "[gis-parcela-find] all_candidates_forbidden siruta=%s cad=%s candidates=%d", + siruta, + cad, + candidates.length, + ); + return NextResponse.json( + { + error: "scope_insufficient", + hint: + "All candidates returned 403 from gis-api — token likely lacks enrichment_scope. Sign out and back in.", + candidates: candidates.length, + }, + { status: 403 }, + ); + } + + console.log("[gis-parcela-find] no_match siruta=%s cad=%s", siruta, cad); return NextResponse.json({ error: "not_found" }, { status: 404 }); } catch (err) { if (err instanceof GisApiError) { diff --git a/src/modules/geoportal/v2/feature-info-panel.tsx b/src/modules/geoportal/v2/feature-info-panel.tsx index 8ff2967..3e20754 100644 --- a/src/modules/geoportal/v2/feature-info-panel.tsx +++ b/src/modules/geoportal/v2/feature-info-panel.tsx @@ -1,9 +1,10 @@ "use client"; import { useEffect, useMemo, useState } from "react"; +import { signIn } from "next-auth/react"; import { X, RefreshCw, Loader2, FileText, Download, AlertCircle, - Home, Building, MapPin, ChevronDown, ChevronRight, Users, + Home, Building, MapPin, ChevronDown, ChevronRight, Users, LogIn, } from "lucide-react"; import { cn } from "@/shared/lib/utils"; @@ -409,9 +410,25 @@ export function FeatureInfoPanel({ feature, onClose, basic = false }: Props) { )} {error === "forbidden" && ( -
- - Nu ai permisiuni de citire detaliată (scope insuficient). +
+
+ + + Token-ul tău nu poartă scope-ul „enrichment" (gis-api a + răspuns 403 pentru toate candidate-le). Re-loghează-te ca + să primești un access_token cu scope-ul corect. + +
+
)} {error && error !== "forbidden" && (