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>
62 lines
1.8 KiB
TypeScript
62 lines
1.8 KiB
TypeScript
import pkceChallenge from 'pkce-challenge'
|
|
import { randomBytes } from 'node:crypto'
|
|
|
|
import type { H3Event } from 'h3'
|
|
|
|
export interface PkceArtifacts {
|
|
codeVerifier: string
|
|
codeChallenge: string
|
|
state: string
|
|
}
|
|
|
|
export async function generatePkce(): Promise<PkceArtifacts> {
|
|
const { code_verifier, code_challenge } = await pkceChallenge()
|
|
const state = randomBytes(16).toString('base64url')
|
|
return {
|
|
codeVerifier: code_verifier,
|
|
codeChallenge: code_challenge,
|
|
state,
|
|
}
|
|
}
|
|
|
|
// ─── PKCE state em Redis (single-use, TTL curto) ─────────────────────────────
|
|
|
|
export interface PkceStateData {
|
|
codeVerifier: string
|
|
returnTo: string
|
|
createdAt: number
|
|
}
|
|
|
|
export async function savePkceState(state: string, data: PkceStateData): Promise<void> {
|
|
const ttl = useRuntimeConfig().pkceTtlSeconds
|
|
await useRedis().set(`pkce:${state}`, JSON.stringify(data), 'EX', ttl)
|
|
}
|
|
|
|
/**
|
|
* Lê e remove o state atomicamente — single-use, previne replay.
|
|
*/
|
|
export async function consumePkceState(state: string): Promise<PkceStateData | null> {
|
|
if (!state) return null
|
|
const key = `pkce:${state}`
|
|
const pipe = useRedis().multi()
|
|
pipe.get(key)
|
|
pipe.del(key)
|
|
const results = await pipe.exec()
|
|
const raw = results?.[0]?.[1] as string | null | undefined
|
|
if (!raw) return null
|
|
try {
|
|
return JSON.parse(raw) as PkceStateData
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Computa o redirect_uri do callback a partir do request — coerente com o que foi enviado ao Keycloak.
|
|
*/
|
|
export function callbackUrlFromEvent(event: H3Event): string {
|
|
const host = getRequestHost(event, { xForwardedHost: true })
|
|
const proto = getRequestProtocol(event, { xForwardedProto: true })
|
|
return `${proto}://${host}/api/auth/callback`
|
|
}
|