fix(geoportal-v2): silent auto re-grant on scope-missing 403
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) <noreply@anthropic.com>
This commit is contained in:
@@ -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<CondoOwner[] | null>(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<boolean>(
|
||||
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) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 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" && (
|
||||
<div className="m-3 flex flex-col gap-2 rounded-md border border-amber-500/30 bg-amber-50 p-2 text-xs text-amber-900 dark:bg-amber-950/30 dark:text-amber-200">
|
||||
<div className="flex items-start gap-2">
|
||||
<AlertCircle className="mt-0.5 h-4 w-4 shrink-0" />
|
||||
<span>
|
||||
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.
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
signIn("authentik", { callbackUrl: window.location.href })
|
||||
}
|
||||
className="inline-flex items-center gap-1 self-start rounded bg-amber-200 px-2 py-1 text-xs font-medium text-amber-950 hover:bg-amber-300 dark:bg-amber-800 dark:text-amber-50 dark:hover:bg-amber-700"
|
||||
>
|
||||
<LogIn className="h-3 w-3" />
|
||||
Re-loghează-te
|
||||
</button>
|
||||
<div className="m-3 flex items-start gap-2 rounded-md border border-amber-500/30 bg-amber-50/50 p-2 text-xs text-amber-900 dark:bg-amber-950/20 dark:text-amber-200">
|
||||
<AlertCircle className="mt-0.5 h-3.5 w-3.5 shrink-0" />
|
||||
<span>Datele detaliate nu pot fi încărcate momentan.</span>
|
||||
</div>
|
||||
)}
|
||||
{error && error !== "forbidden" && (
|
||||
|
||||
Reference in New Issue
Block a user