All checks were successful
Dev Build & Deploy Portal / build-deploy (push) Successful in 2m58s
Integra débitos, pagamentos e guias emitidas com API via composables e modais de extrato. Simplifica filtros do portal ao escopo do contribuinte logado. Refatora emissão pública de certidão com modelos dinâmicos e contrato idModelo. Corrige status de taxas (2=Paga, 3=Cancelada) e melhorias no proxy BFF/Keycloak. Co-authored-by: Cursor <cursoragent@cursor.com>
103 lines
4.1 KiB
TypeScript
103 lines
4.1 KiB
TypeScript
/**
|
|
* Proxy genérico para o core-api.
|
|
*
|
|
* Fluxo:
|
|
* 1. Lê cookie de sessão → busca tokens em Redis (refresh transparente se expirado)
|
|
* 2. Resolve o tenant (dominio) a partir do hostname
|
|
* 3. Busca codigoMunicipio via /publico/prefeitura/{dominio} (cacheado em Redis)
|
|
* 4. Forward para core-api com Authorization + X-Municipio + X-Dominio
|
|
*
|
|
* Bypass: rotas /api/v1/publico/** podem ser acessadas sem sessão.
|
|
*/
|
|
export default defineEventHandler(async (event) => {
|
|
const path = getRouterParam(event, 'path') ?? ''
|
|
const isPublico = path.startsWith('publico/')
|
|
|
|
let accessToken: string | null = null
|
|
if (!isPublico) {
|
|
const sid = readSessionCookie(event)
|
|
if (!sid) {
|
|
throw createError({ statusCode: 401, statusMessage: 'Sem sessão' })
|
|
}
|
|
accessToken = await getValidAccessToken(sid)
|
|
if (!accessToken) {
|
|
clearSessionCookie(event)
|
|
throw createError({ statusCode: 401, statusMessage: 'Sessão expirada' })
|
|
}
|
|
}
|
|
|
|
const dominio = tenantFromEvent(event)
|
|
const prefeitura = await fetchPrefeituraInfo(dominio)
|
|
if (!prefeitura) {
|
|
throw createError({ statusCode: 400, statusMessage: `Tenant '${dominio}' não encontrado` })
|
|
}
|
|
|
|
const cfg = useRuntimeConfig()
|
|
const url = `${cfg.coreApiUrl}/api/v1/${path}`
|
|
const query = getQuery(event)
|
|
const method = event.method.toUpperCase()
|
|
const body = ['GET', 'HEAD'].includes(method) ? undefined : await readRawBody(event)
|
|
|
|
const headers: Record<string, string> = {
|
|
'X-Municipio': String(prefeitura.codigoMunicipio),
|
|
'X-Dominio': prefeitura.dominio,
|
|
}
|
|
if (accessToken) headers.Authorization = `Bearer ${accessToken}`
|
|
const contentType = getHeader(event, 'content-type')
|
|
if (contentType) headers['Content-Type'] = contentType
|
|
|
|
console.log(`[proxy] ${method} ${url} | X-Municipio: ${headers['X-Municipio']} | X-Dominio: ${headers['X-Dominio']} | auth: ${!!accessToken}`)
|
|
|
|
try {
|
|
const res = await $fetch.raw(url, {
|
|
method: method as 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE',
|
|
query,
|
|
body,
|
|
headers,
|
|
responseType: 'stream',
|
|
})
|
|
|
|
setResponseStatus(event, res.status)
|
|
const skipResponseHeaders = new Set(['transfer-encoding', 'content-encoding', 'content-length'])
|
|
for (const [name, value] of res.headers.entries()) {
|
|
if (skipResponseHeaders.has(name.toLowerCase())) continue
|
|
setResponseHeader(event, name, value)
|
|
}
|
|
return res._data
|
|
} catch (err: unknown) {
|
|
const fetchErr = err as { response?: { status?: number; _data?: unknown } }
|
|
if (fetchErr.response) {
|
|
const status = fetchErr.response.status ?? 500
|
|
const raw = fetchErr.response._data
|
|
|
|
// responseType: 'stream' faz _data ser ReadableStream mesmo em erros —
|
|
// lemos o stream e parseamos como JSON para que o cliente veja o envelope de erro
|
|
let body: unknown = raw
|
|
if (raw && typeof (raw as ReadableStream).getReader === 'function') {
|
|
const reader = (raw as ReadableStream<Uint8Array>).getReader()
|
|
const chunks: Uint8Array[] = []
|
|
for (;;) {
|
|
const { value, done } = await reader.read()
|
|
if (done) break
|
|
if (value) chunks.push(value)
|
|
}
|
|
const totalLen = chunks.reduce((n, c) => n + c.length, 0)
|
|
const merged = new Uint8Array(totalLen)
|
|
let offset = 0
|
|
for (const c of chunks) { merged.set(c, offset); offset += c.length }
|
|
const text = new TextDecoder().decode(merged)
|
|
try { body = JSON.parse(text) } catch { body = text }
|
|
}
|
|
|
|
if (import.meta.dev) {
|
|
console.error(`[proxy] ERRO ${status} ← ${url}`, body)
|
|
}
|
|
|
|
setResponseStatus(event, status)
|
|
setResponseHeader(event, 'content-type', 'application/json; charset=utf-8')
|
|
return body
|
|
}
|
|
throw err
|
|
}
|
|
})
|