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>
41 lines
1.4 KiB
TypeScript
41 lines
1.4 KiB
TypeScript
export default defineEventHandler(async (event) => {
|
|
const { code, state, error, error_description } = getQuery(event) as Record<string, string | undefined>
|
|
|
|
if (error) {
|
|
console.warn('[auth/callback] Keycloak retornou erro:', error, error_description)
|
|
return sendRedirect(event, `/?auth_error=${encodeURIComponent(error)}`, 302)
|
|
}
|
|
|
|
if (!code || !state) {
|
|
return sendRedirect(event, '/?auth_error=missing_params', 302)
|
|
}
|
|
|
|
const pkceState = await consumePkceState(state)
|
|
if (!pkceState) {
|
|
return sendRedirect(event, '/?auth_error=invalid_state', 302)
|
|
}
|
|
|
|
try {
|
|
const tokens = await exchangeCodeForTokens({
|
|
code,
|
|
codeVerifier: pkceState.codeVerifier,
|
|
redirectUri: callbackUrlFromEvent(event),
|
|
})
|
|
|
|
const userInfo = userInfoFromAccessToken(tokens.access_token)
|
|
const sid = await createSession({
|
|
accessToken: tokens.access_token,
|
|
refreshToken: tokens.refresh_token,
|
|
idToken: tokens.id_token,
|
|
accessTokenExpiresAt: Date.now() + tokens.expires_in * 1000,
|
|
userInfo,
|
|
})
|
|
|
|
setSessionCookie(event, sid)
|
|
return sendRedirect(event, pkceState.returnTo, 302)
|
|
} catch (err) {
|
|
console.error('[auth/callback] exchange falhou:', (err as Error).message)
|
|
return sendRedirect(event, '/?auth_error=exchange_failed', 302)
|
|
}
|
|
})
|