gabrielb 71e1a3f970 feat: portal Nuxt 3 com BFF + autenticação Keycloak (Fase 1)
Substitui o portal Vite+Vue puro por Nuxt 3 com BFF embutido (Nitro server
routes) e fluxo de autenticação Keycloak via token-handler pattern.

Server (BFF):
- server/api/auth/{login,callback,refresh,logout,me}.ts — Keycloak PKCE
- server/api/proxy/[...path].ts — proxy autenticado pro core-api com tenant
- server/utils/{session,keycloak,pkce,redis,tenant,prefeitura}.ts
- server/middleware/csrf.ts — Origin check + header X-Requested-With

Auth (token-handler pattern):
- JWT vive só server-side em Redis; cliente recebe cookie session-id opaco
- Refresh transparente quando access_token expira
- Multi-tenant via hostname → X-Municipio/X-Dominio injetados no proxy
- Realm dedicado: modumfiscal-portal-{env}

Frontend (Nuxt):
- src/pages/** (file-based routing) substitui src/views/
- Plugins SSR: prefeitura (bootstrap pré-hidratação) + auth (hidrata user via /api/auth/me)
- Composables useAuth, useApi, useLoginModal, useFocusLoginInput
- Modal global de login quando middleware /portal/** bloqueia
- Splash overlay no boot esconde flash do preset inicial pro tema dinâmico
- DocumentoInput bloqueia campo quando user autenticado (pré-preenche em certidão/IPTU)

Removidos:
- index.html, vite.config.js, src/main.js, src/router/
- src/config/apiClient.js (substituído por \$fetch via /api/proxy)
- src/services/{auth,prefeitura}Service.js (lógica migrada pra composables/plugins)
- src/mocks/ (não mais usado)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 20:31:19 -03:00

45 lines
1.2 KiB
TypeScript

export interface PrefeituraInfo {
codigoMunicipio: number
nomePrefeitura: string
dominio: string
template: string
pathLogo?: string
pathBackground?: string
}
interface ApiEnvelope<T> {
data: T
}
const CACHE_TTL_SECONDS = 300 // 5 min
export async function fetchPrefeituraInfo(dominio: string): Promise<PrefeituraInfo | null> {
if (!dominio) return null
const cacheKey = `prefeitura:${dominio}`
const cached = await useRedis().get(cacheKey)
if (cached) {
try {
return JSON.parse(cached) as PrefeituraInfo
} catch {
// cache corrompido — segue para refetch
}
}
const cfg = useRuntimeConfig()
try {
const res = await $fetch<ApiEnvelope<PrefeituraInfo>>(
`${cfg.coreApiUrl}/api/v1/publico/prefeitura/${encodeURIComponent(dominio)}`,
{ timeout: 8000 },
)
const info = res?.data
if (!info?.codigoMunicipio) return null
await useRedis().set(cacheKey, JSON.stringify(info), 'EX', CACHE_TTL_SECONDS)
return info
} catch (err) {
console.error(`[prefeitura] lookup falhou para '${dominio}':`, (err as Error).message)
return null
}
}