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>
77 lines
2.3 KiB
Vue
77 lines
2.3 KiB
Vue
<script setup>
|
|
import { ref, computed, watch } from 'vue'
|
|
|
|
const props = defineProps({
|
|
modelValue: { type: String, default: '' },
|
|
disabled: { type: Boolean, default: false },
|
|
})
|
|
const emit = defineEmits(['update:modelValue'])
|
|
|
|
const wrapper = ref(null)
|
|
const input = ref(props.modelValue)
|
|
|
|
watch(() => props.modelValue, (v) => { input.value = v })
|
|
|
|
const apenasDigitos = computed(() => input.value.replace(/\D/g, ''))
|
|
const isCnpj = computed(() => apenasDigitos.value.length > 11)
|
|
|
|
const valorFormatado = computed(() => {
|
|
const d = apenasDigitos.value
|
|
if (d.length <= 11) {
|
|
return d
|
|
.replace(/(\d{3})(\d)/, '$1.$2')
|
|
.replace(/(\d{3})(\d)/, '$1.$2')
|
|
.replace(/(\d{3})(\d{1,2})$/, '$1-$2')
|
|
}
|
|
return d
|
|
.replace(/(\d{2})(\d)/, '$1.$2')
|
|
.replace(/(\d{3})(\d)/, '$1.$2')
|
|
.replace(/(\d{3})(\d)/, '$1/$2')
|
|
.replace(/(\d{4})(\d{1,2})$/, '$1-$2')
|
|
})
|
|
|
|
const placeholder = computed(() => isCnpj.value ? '00.000.000/0000-00' : '000.000.000-00')
|
|
|
|
function onInput(e) {
|
|
const raw = e.target.value.replace(/\D/g, '').slice(0, 14)
|
|
input.value = raw
|
|
emit('update:modelValue', raw)
|
|
}
|
|
|
|
function focus() {
|
|
wrapper.value?.querySelector('input')?.focus()
|
|
}
|
|
|
|
defineExpose({ focus })
|
|
</script>
|
|
|
|
<template>
|
|
<div ref="wrapper" class="relative">
|
|
<InputText
|
|
:value="valorFormatado"
|
|
:placeholder="placeholder"
|
|
:disabled="disabled"
|
|
inputmode="numeric"
|
|
autocomplete="username"
|
|
class="w-full text-lg tracking-wide"
|
|
size="large"
|
|
aria-label="CPF ou CNPJ"
|
|
aria-required="true"
|
|
@input="onInput"
|
|
/>
|
|
<span
|
|
v-if="disabled"
|
|
class="absolute right-3 top-1/2 -translate-y-1/2 text-xs text-slate-400 font-medium flex items-center gap-1 pointer-events-none"
|
|
>
|
|
<i class="pi pi-lock text-[10px]" aria-hidden="true" />
|
|
{{ isCnpj ? 'CNPJ' : 'CPF' }}
|
|
</span>
|
|
<span
|
|
v-else-if="apenasDigitos.length > 0"
|
|
class="absolute right-3 top-1/2 -translate-y-1/2 text-xs text-slate-400 font-medium pointer-events-none"
|
|
>
|
|
{{ isCnpj ? 'CNPJ' : 'CPF' }}
|
|
</span>
|
|
</div>
|
|
</template>
|