THE bug behind every "data nu raman" / invalid_token incident this
session: refresh POSTed to `{issuer}/token/` = /application/o/architools/token/
which returns HTTP 405 + empty body. JSON.parse on the empty body
threw "Unexpected end of JSON input" → catch fired → token marked
RefreshAccessTokenError → 60s cooldown later, retry hit the same
broken URL → loop.
OIDC discovery at {issuer}/.well-known/openid-configuration confirms:
"token_endpoint": "https://auth.beletage.ro/application/o/token/"
This is the SHARED endpoint, not per-provider. Hard-fix the URL by
constructing it from the issuer's origin.
Marius's currently-stuck session will auto-recover on next request
(cooldown expires, refresh fires against the corrected URL,
refresh_token still valid 30d).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previous logic set token.error=RefreshAccessTokenError and never
retried — once a refresh failed (likely a race during the early
parallel-storm period), Marius's JWT cookie carried that error
forever. New jwt calls all saw "blocked" → kept using the stale
accessToken → api.gis.ac returned invalid_token on every call.
Fix: store errorAt timestamp alongside the error flag. Block refresh
attempts for 60s after a failure (avoids hot-loop on persistent
Authentik issues), then unblock and retry. On the next failure, the
60s cooldown re-arms.
For Marius's currently-stuck session: as soon as this deploys, his
next jwt callback will pass the cooldown check (errorAt is hours ago)
and trigger a fresh refresh. If Authentik is happy with his
refresh_token, the error flag is cleared and he's back to normal —
no relogin needed.
Logs now show "blocked=true/false" alongside secLeft for visibility.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
UAT click previously console.logged only. gis-api search response
doesn't include bbox/centroid, so ArchiTools can't fitBounds locally.
Reuse the deep-link pattern (already used by Export GeoPackage) →
open eterra.live/harta?siruta=X in a new tab. eterra.live has its own
/api/geoportal/uat-bounds + flyTo wired.
Future: add GET /api/v1/uat/:siruta/bounds to gis-api so ArchiTools
can fitBounds inline without leaving the page.
Also reverts the session.debug diagnostic (Marius confirmed
hasRefreshToken=true + expiresIn=293 after attaching offline_access
scope mapping to Authentik provider pk=6 — root cause fixed,
diagnostic no longer needed).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Temporary — verify whether Marius's JWT has a refresh_token. Will
revert once cause is identified.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 fixes for the symptoms Marius hit on Faza E pilot — search returning
"Eroare la căutare" after sessions got stale, even after relogin:
1. Refresh deduplication
Authentik rotates refresh_tokens — exchange-once. Parallel map +
search + parcela.get all hit jwt callback concurrently, each fires
its own refresh, the first wins, the rest get invalid_grant and
poison the JWT with token.error=RefreshAccessTokenError → user
appears logged out for no good reason. Cache the inflight refresh
promise in-memory keyed by refresh_token so concurrent callers
share one Authentik exchange.
2. Fetch timeout in gis-api-client
AbortSignal.timeout(30s) on every api.gis.ac call. Without it, a
slow upstream (ANCPI scrape, orchestrator hiccup) hangs the route
for the full Next.js default → Marius saw 10s gaps with no
feedback. Throws GisApiError(504, upstream_timeout) instead.
3. Better error surfacing
/api/gis/* routes return { error, hint: <first 200 chars> } on
non-GisApiError throws instead of a bare "internal_error". Easier
to triage from browser DevTools without paging through container
logs.
4. Remove diagnostic [gis-search] logs
Diagnostic served its purpose (identified the stale-token cause
pre-refresh-fix). Now noise; keep only [auth] refresh success/fail
+ per-route internal_error.
Also adds AbortSignal.timeout(8s) on the Authentik refresh fetch
itself to keep the jwt callback bounded.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Authentik provider pk=6 issues access tokens with 5-minute TTL but
NextAuth's JWT cookie lives 30 days. Without refresh, every api.gis.ac
call after the first 5 minutes returned 401 invalid_token — the exact
failure Marius hit on first Faza E pilot test.
Implementation:
- jwt callback captures account.refresh_token + account.expires_at on
first sign-in alongside access_token.
- Before each jwt issuance, if access_token is within 30s of expiry
and a refresh_token exists, POST to {issuer}/token/ with
grant_type=refresh_token + client_id + client_secret. Update token
with the new access_token + expiry + (rotated) refresh_token.
- On failure, set token.error="RefreshAccessTokenError" and stop
trying (avoid hot-loop). Surfaced via session.error so client UI
can prompt re-login.
AUTHENTIK_SCOPES updated in Infisical to include `offline_access` so
Authentik issues a refresh_token on first sign-in (standard OAuth2).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Server-side helper useGisAcFlag(email) → boolean, gated by:
- USE_GIS_AC=1 (global rollout switch), OR
- GIS_AC_PILOT_USERS=a@x,b@y (per-email staged rollout)
Both defaults are off (USE_GIS_AC=0, pilot list empty) in Infisical
/architools — this PR is dormant; no call sites consume the flag yet.
Future Faza D/E call sites in src/lib/gis-api-client.ts and
src/modules/geoportal/* will branch on it.
Exposed on session.useGisAc so client components can branch identically
to server routes without a separate API roundtrip. Re-evaluated per
request → flag flip via Infisical + container restart, no rebuild.
Per-user override (PILOT_USERS) is the rollout vehicle:
1. Deploy with flag=0 (default) → nothing changes
2. Set GIS_AC_PILOT_USERS=marius@... → Marius sees new code path
3. Watch 24-48h → set USE_GIS_AC=1 → global cutover
4. Rollback = unset USE_GIS_AC
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Authentik OIDC provider now requests `openid email profile enrichment`
(from AUTHENTIK_SCOPES env, Infisical-fetched at boot). The enrichment
scope triggers Authentik scope mapping pk=41b23bc3-bdd8-4a61-b975-
6e0eff56df72 which emits enrichment_scope + is_beletage_group claims
based on LDAP group membership (Arhitecti/Administrators/Domain Admins
→ scope=full + is_beletage_group=true).
jwt callback captures account.access_token on first sign-in; session
callback exposes it as session.accessToken so api.gis.ac calls can
forward it. Used by Faza D thin client (src/lib/gis-api-client.ts,
pending) to authenticate against api.gis.ac.
Without scope=enrichment, every architools user falls through to
scope=none on api.gis.ac → 403 on every parcel/enrichment read.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Auth:
- Add middleware.ts that redirects unauthenticated users to Authentik SSO
- Extract authOptions to shared auth-options.ts
- Add getAuthSession() helper for API route protection
- Add loading spinner during session validation
- Dev mode bypasses auth (stub user still works)
ManicTime:
- Fix hardcoded companyId="beletage" — now uses group context from Tags.txt
- Fix extended project format label parsing (extracts name after year)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>