Commit Graph

10 Commits

Author SHA1 Message Date
Claude VM 162c8ed257 fix(auth): Authentik token endpoint is /application/o/token/ (shared)
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>
2026-05-19 08:23:43 +03:00
Claude VM 293d15edf2 fix(auth): refresh cooldown 60s — auto-recover from sticky errors
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>
2026-05-19 07:59:53 +03:00
Claude VM afef778612 debug(auth): log jwt callback state + re-expose session.debug
Still hitting invalid_token. Need to see jwt callback behavior live —
why is refresh not firing for Marius?
2026-05-19 07:31:06 +03:00
Claude VM 68355efbba fix(geoportal-v2): UAT click deep-links to eterra.live + revert debug
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>
2026-05-18 23:59:47 +03:00
Claude VM 1c6efb9d78 debug(auth): expose session.debug={hasRefreshToken, expiresIn}
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>
2026-05-18 23:46:47 +03:00
Claude VM 6054d083b5 fix(faza-e): refresh dedup, fetch timeout, error surfacing
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>
2026-05-18 22:58:16 +03:00
Claude VM 47ca366984 fix(auth): Authentik access_token refresh flow
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>
2026-05-18 22:13:19 +03:00
Claude VM 977db6d63a feat(cutover): Faza C feature-flag infra for api.gis.ac
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>
2026-05-18 08:16:43 +03:00
Claude VM 403b6b37f1 feat(auth): Faza B NextAuth Authentik scope=enrichment + forward access_token
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>
2026-05-18 08:01:16 +03:00
AI Assistant ca4d7b5d8d feat(auth): force Authentik login on first visit, fix ManicTime sync
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>
2026-03-09 12:26:08 +02:00