GUILHERME e4c468e61e
All checks were successful
Dev Build & Deploy Portal / build-deploy (push) Successful in 2m58s
feat(portal): extratos reais, certidão dinâmica e filtros self-scoped
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>
2026-05-22 16:21:59 -03:00

116 lines
3.7 KiB
TypeScript

export interface TokenResponse {
access_token: string
refresh_token: string
id_token: string
expires_in: number
refresh_expires_in?: number
token_type: string
scope?: string
}
function realmBase(): string {
const cfg = useRuntimeConfig()
return `${cfg.keycloakUrl}/realms/${cfg.keycloakRealm}`
}
export function buildAuthUrl(opts: {
codeChallenge: string
state: string
redirectUri: string
loginHint?: string
primary?: string
dark?: boolean
}): string {
const cfg = useRuntimeConfig()
const params = new URLSearchParams({
client_id: cfg.keycloakClientId,
redirect_uri: opts.redirectUri,
response_type: 'code',
scope: 'openid profile email',
code_challenge: opts.codeChallenge,
code_challenge_method: 'S256',
state: opts.state,
})
if (opts.loginHint) params.set('login_hint', opts.loginHint)
if (opts.primary) params.set('primary', opts.primary)
if (opts.dark !== undefined) params.set('dark', String(opts.dark))
return `${realmBase()}/protocol/openid-connect/auth?${params.toString()}`
}
export async function exchangeCodeForTokens(opts: {
code: string
codeVerifier: string
redirectUri: string
}): Promise<TokenResponse> {
const cfg = useRuntimeConfig()
const body = new URLSearchParams({
grant_type: 'authorization_code',
code: opts.code,
client_id: cfg.keycloakClientId,
client_secret: cfg.keycloakClientSecret,
redirect_uri: opts.redirectUri,
code_verifier: opts.codeVerifier,
})
try {
return await $fetch<TokenResponse>(
`${realmBase()}/protocol/openid-connect/token`,
{
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: body.toString(),
},
)
} catch (err: unknown) {
const fetchErr = err as { data?: { error?: string; error_description?: string }; message?: string }
const kcError = fetchErr.data?.error
const kcDesc = fetchErr.data?.error_description
const detail = kcDesc ?? kcError ?? fetchErr.message ?? 'erro desconhecido'
throw new Error(`Keycloak token: ${detail}`)
}
}
export async function refreshTokens(refreshToken: string): Promise<TokenResponse> {
const cfg = useRuntimeConfig()
const body = new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: refreshToken,
client_id: cfg.keycloakClientId,
client_secret: cfg.keycloakClientSecret,
})
return await $fetch<TokenResponse>(
`${realmBase()}/protocol/openid-connect/token`,
{
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: body.toString(),
},
)
}
export function buildLogoutUrl(opts: {
idTokenHint: string
postLogoutRedirectUri: string
}): string {
const params = new URLSearchParams({
id_token_hint: opts.idTokenHint,
post_logout_redirect_uri: opts.postLogoutRedirectUri,
})
return `${realmBase()}/protocol/openid-connect/logout?${params.toString()}`
}
/**
* Decodifica payload de JWT sem validar assinatura — uso server-only para
* extrair claims do id_token/access_token recém-emitido pelo Keycloak.
* NÃO USAR para validar tokens recebidos de terceiros.
*/
export function decodeJwtPayload(token: string): Record<string, unknown> {
const [, payload] = token.split('.')
if (!payload) return {}
try {
const json = Buffer.from(payload, 'base64url').toString('utf-8')
return JSON.parse(json) as Record<string, unknown>
} catch {
return {}
}
}