fix(geoportal-v2): surface scope-insufficient instead of silent 404

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) <noreply@anthropic.com>
This commit is contained in:
Claude VM
2026-05-19 16:52:47 +03:00
parent 8ff67d19fb
commit 71df1ee9ec
2 changed files with 54 additions and 6 deletions
@@ -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" && (
<div className="m-3 flex items-start 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">
<AlertCircle className="mt-0.5 h-4 w-4 shrink-0" />
<span>Nu ai permisiuni de citire detaliată (scope insuficient).</span>
<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>
)}
{error && error !== "forbidden" && (