From a23ba1957fe517325fd954bf5357768cf3356739 Mon Sep 17 00:00:00 2001 From: Claude VM Date: Tue, 19 May 2026 16:57:42 +0300 Subject: [PATCH] fix(geoportal-v2): silent auto re-grant on scope-missing 403 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removes the "Re-loghează-te" button + scope-mismatch warning prose. On 403 from /api/gis/parcela/find the panel now: 1. Checks sessionStorage flag — false on first 403 of the tab 2. Sets the flag, fires signIn("authentik", { callbackUrl: current URL }) silently. For an SSO'd user this is a sub-second Authentik redirect cycle that mints a fresh access_token with the right scope claims, lands the user back on the same panel, and the re-mount fetches successfully — no visible message, no prompt. 3. If another 403 happens after the retry (i.e., Authentik genuinely can't grant the scope — config issue, not a stale-token issue), falls through to a discreet "Datele detaliate nu pot fi încărcate momentan." note. No call-to-action, no jargon. 4. On any successful 200 fetch, clears the sessionStorage flag so a future 403 in the same tab can re-trigger the silent retry. Per Marius: "vreau doar să meargă, safe și fix" — no auth-flow chrome shown to the user. The recovery is part of the system's correctness contract, not a feature for the user to manage. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../geoportal/v2/feature-info-panel.tsx | 61 ++++++++++++------- 1 file changed, 40 insertions(+), 21 deletions(-) diff --git a/src/modules/geoportal/v2/feature-info-panel.tsx b/src/modules/geoportal/v2/feature-info-panel.tsx index 3e20754..90b8df4 100644 --- a/src/modules/geoportal/v2/feature-info-panel.tsx +++ b/src/modules/geoportal/v2/feature-info-panel.tsx @@ -1,13 +1,20 @@ "use client"; -import { useEffect, useMemo, useState } from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; import { signIn } from "next-auth/react"; import { X, RefreshCw, Loader2, FileText, Download, AlertCircle, - Home, Building, MapPin, ChevronDown, ChevronRight, Users, LogIn, + Home, Building, MapPin, ChevronDown, ChevronRight, Users, } from "lucide-react"; import { cn } from "@/shared/lib/utils"; +// Once-per-tab marker: when we trigger a silent re-grant via signIn() the +// page reloads through Authentik. After the round-trip we land back on the +// same panel; we don't want to retry the re-grant in an infinite loop if +// Authentik actually can't extend the scope, so we set a sessionStorage +// flag before redirecting and check it on mount. +const AUTH_RETRY_KEY = "gis_panel_auth_retry"; + export interface ClickedFeatureLite { /** GisFeature uuid — typically empty from PMTiles overview, falls back to search-by-cadref */ id: string; @@ -152,6 +159,13 @@ export function FeatureInfoPanel({ feature, onClose, basic = false }: Props) { const [condoOwners, setCondoOwners] = useState(null); const [condoLoading, setCondoLoading] = useState(false); const [condoExpanded, setCondoExpanded] = useState(false); + // Tracks whether we've already triggered a silent re-grant for this tab, + // so a real Authentik scope misconfig falls through to the muted error + // state instead of cycling forever through signIn(). + const authRetriedRef = useRef( + typeof window !== "undefined" && + sessionStorage.getItem(AUTH_RETRY_KEY) === "1", + ); const isCladiri = feature.layerId === "CLADIRI_ACTIVE"; @@ -186,10 +200,28 @@ export function FeatureInfoPanel({ feature, onClose, basic = false }: Props) { return; } if (r.status === 403) { + // Token reaches gis-api but lacks enrichment_scope. Trigger a + // silent fresh OIDC grant — for SSO'd users Authentik is a + // sub-second redirect, the panel re-mounts with full scope and + // shows real data. authRetriedRef guards against infinite loop + // if Authentik genuinely can't grant the scope. + if (!authRetriedRef.current && typeof window !== "undefined") { + authRetriedRef.current = true; + sessionStorage.setItem(AUTH_RETRY_KEY, "1"); + void signIn("authentik", { callbackUrl: window.location.href }); + // Keep "loading" while the browser navigates — no message + // needed; the redirect itself is the feedback. + return; + } setError("forbidden"); setLoading(false); return; } + // Successful fetch (200) — clear the retry marker so a future 403 + // can re-trigger silently. + if (typeof window !== "undefined") { + sessionStorage.removeItem(AUTH_RETRY_KEY); + } if (!r.ok) { setError("fetch_failed"); setLoading(false); @@ -409,26 +441,13 @@ export function FeatureInfoPanel({ feature, onClose, basic = false }: Props) { )} + {/* Forbidden after a silent re-grant attempt already failed + once — likely Authentik scope misconfig. Stays discreet (no + call-to-action), only diag info for the operator. */} {error === "forbidden" && ( -
-
- - - 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. - -
- +
+ + Datele detaliate nu pot fi încărcate momentan.
)} {error && error !== "forbidden" && (