feat/nuxt-bff #1

Merged
gabrielb merged 3 commits from feat/nuxt-bff into developer 2026-05-19 00:22:54 +00:00
3 changed files with 28 additions and 157 deletions
Showing only changes of commit 8905e7f27c - Show all commits

View File

@ -11,11 +11,12 @@ const { prefersReducedMotion } = useMotion()
const router = useRouter() const router = useRouter()
const prefeitura = usePrefeituraStore() const prefeitura = usePrefeituraStore()
const { isAuthenticated, nomeUsuario } = useAuth() const { isAuthenticated, nomeUsuario, login } = useAuth()
const { requested: focusLoginRequested, consume: consumeFocusLogin } = useFocusLoginInput() const { requested: focusLoginRequested, consume: consumeFocusLogin } = useFocusLoginInput()
const documento = ref('') const documento = ref('')
const erro = ref('') const erro = ref('')
const carregando = ref(false)
// Ref ao DocumentoInput usado pelo botão "Entrar" do AppHeader pra focar o campo // Ref ao DocumentoInput usado pelo botão "Entrar" do AppHeader pra focar o campo
const documentoRef = ref(null) const documentoRef = ref(null)
@ -103,13 +104,21 @@ const servicosAutenticados = [
{ icon: 'pi-user', titulo: 'Dados Cadastrais', descricao: 'Visualize e mantenha seus dados sempre atualizados.', to: '/portal/dados' }, { icon: 'pi-user', titulo: 'Dados Cadastrais', descricao: 'Visualize e mantenha seus dados sempre atualizados.', to: '/portal/dados' },
] ]
function continuar() { async function continuar() {
if (documento.value.replace(/\D/g, '').length < 11) { const doc = documento.value.replace(/\D/g, '')
if (doc.length < 11) {
erro.value = 'Informe um CPF (11 dígitos) ou CNPJ (14 dígitos) válido.' erro.value = 'Informe um CPF (11 dígitos) ou CNPJ (14 dígitos) válido.'
return return
} }
erro.value = '' erro.value = ''
router.push({ path: '/login', query: { doc: documento.value } }) carregando.value = true
try {
await login(doc, '/portal/painel')
// login() faz window.location não retorna aqui em condições normais
} catch (e) {
carregando.value = false
erro.value = e?.data?.statusMessage ?? 'Não foi possível iniciar o login.'
}
} }
</script> </script>
@ -257,6 +266,7 @@ function continuar() {
icon-pos="right" icon-pos="right"
class="w-full" class="w-full"
size="large" size="large"
:loading="carregando"
@click="continuar" @click="continuar"
/> />
</div> </div>

View File

@ -1,152 +0,0 @@
<script setup>
import { ref, computed } from 'vue'
import { useAuth } from '@/composables/useAuth'
const route = useRoute()
const router = useRouter()
const { login } = useAuth()
const carregando = ref(false)
const erro = ref('')
const docBruto = computed(() => (route.query.doc ?? '').toString().replace(/\D/g, ''))
const docFormatado = computed(() => {
const d = docBruto.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 tipoDoc = computed(() => docBruto.value.length > 11 ? 'CNPJ' : 'CPF')
function trocarDocumento() {
router.push('/')
}
async function entrar() {
if (!docBruto.value) {
erro.value = 'Documento não informado. Volte e informe seu CPF/CNPJ.'
return
}
erro.value = ''
carregando.value = true
try {
await login(docBruto.value, '/portal/painel')
// login() faz window.location para o Keycloak não retorna aqui
} catch (e) {
carregando.value = false
erro.value = e?.data?.statusMessage ?? 'Não foi possível iniciar o login. Tente novamente.'
}
}
</script>
<template>
<div class="min-h-[calc(100vh-8rem)] flex items-center justify-center px-4 py-12">
<div class="w-full max-w-md">
<div class="bg-white dark:bg-slate-800 rounded-2xl shadow-lg border border-slate-200 dark:border-slate-700 overflow-hidden">
<div class="bg-gradient-to-r from-primary-700 to-primary-800 px-8 py-6 bg-primary">
<div class="flex items-center gap-3">
<div class="w-10 h-10 bg-white/20 rounded-xl flex items-center justify-center" aria-hidden="true">
<i class="pi pi-lock text-white text-lg" />
</div>
<div>
<h1 class="text-white font-bold text-base leading-tight">Acesso seguro</h1>
<p class="text-white/80 text-xs mt-0.5">Portal do Contribuinte</p>
</div>
</div>
</div>
<div class="px-8 py-8 space-y-6">
<!-- Documento identificado -->
<div>
<p class="text-xs font-semibold text-slate-600 dark:text-slate-400 uppercase tracking-wide mb-2">
Entrando como
</p>
<div class="flex items-center justify-between bg-slate-50 dark:bg-slate-700 border border-slate-200 dark:border-slate-600 rounded-xl px-4 py-3">
<div class="flex items-center gap-3">
<div class="w-8 h-8 bg-primary/10 rounded-lg flex items-center justify-center" aria-hidden="true">
<i class="pi pi-id-card text-primary text-sm" />
</div>
<div>
<p class="font-mono font-semibold text-slate-800 dark:text-slate-100 text-sm">{{ docFormatado }}</p>
<p class="text-xs text-slate-600 dark:text-slate-300">{{ tipoDoc }}</p>
</div>
</div>
<button
class="inline-flex items-center gap-1.5 text-sm text-primary hover:text-primary font-semibold px-3 py-3 rounded-lg hover:bg-primary/8 transition-colors"
aria-label="Trocar documento de identificação"
@click="trocarDocumento"
>
<i class="pi pi-pencil text-xs" aria-hidden="true" />
Trocar
</button>
</div>
</div>
<p class="text-sm text-slate-600 dark:text-slate-300 leading-relaxed">
Você será redirecionado para o serviço de autenticação seguro
onde deverá digitar sua senha.
</p>
<p
v-if="erro"
role="alert"
aria-live="assertive"
class="text-sm text-red-700 font-medium flex items-center gap-1.5"
>
<i class="pi pi-exclamation-circle text-sm" aria-hidden="true" />
{{ erro }}
</p>
<Button
label="Continuar para autenticação"
icon="pi pi-sign-in"
class="w-full"
size="large"
:loading="carregando"
@click="entrar"
/>
<div class="text-center space-y-1">
<NuxtLink
to="/primeiro-acesso"
class="block text-sm text-primary font-medium py-2 hover:underline"
>
Esqueci minha senha
</NuxtLink>
<NuxtLink
to="/credenciamento"
class="block text-sm text-slate-600 dark:text-slate-400 py-2 hover:text-slate-800 dark:hover:text-slate-200 transition-colors"
>
Ainda não tem acesso? <span class="text-primary font-semibold">Credenciar-se</span>
</NuxtLink>
</div>
</div>
</div>
<div class="text-center mt-4">
<button
class="inline-flex items-center gap-2 text-sm text-slate-600 dark:text-slate-400 hover:text-slate-800 dark:hover:text-slate-200 transition-colors py-3 px-4 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-800"
@click="router.push('/')"
>
<i class="pi pi-arrow-left text-xs" aria-hidden="true" />
Voltar à página inicial
</button>
</div>
</div>
</div>
</template>

View File

@ -1,8 +1,10 @@
<script setup> <script setup>
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
import { useAuth } from '@/composables/useAuth'
import { primeiroAcessoService } from '@/services/primeiroAcessoService' import { primeiroAcessoService } from '@/services/primeiroAcessoService'
const router = useRouter() const router = useRouter()
const { login } = useAuth()
const etapa = ref(0) const etapa = ref(0)
const carregando = ref(false) const carregando = ref(false)
@ -86,6 +88,17 @@ async function definirSenha() {
} }
} }
async function entrarKeycloak() {
carregando.value = true
erro.value = ''
try {
await login(docDigitos.value, '/portal/painel')
} catch (e) {
carregando.value = false
erro.value = e?.data?.statusMessage ?? 'Não foi possível iniciar o login.'
}
}
const iconeCanal = { EMAIL: 'pi-envelope', SMS: 'pi-mobile', WHATSAPP: 'pi-whatsapp' } const iconeCanal = { EMAIL: 'pi-envelope', SMS: 'pi-mobile', WHATSAPP: 'pi-whatsapp' }
const labelCanal = { EMAIL: 'E-mail', SMS: 'SMS', WHATSAPP: 'WhatsApp' } const labelCanal = { EMAIL: 'E-mail', SMS: 'SMS', WHATSAPP: 'WhatsApp' }
</script> </script>
@ -224,7 +237,7 @@ const labelCanal = { EMAIL: 'E-mail', SMS: 'SMS', WHATSAPP: 'WhatsApp' }
Você pode acessar o portal com seu CPF/CNPJ e a nova senha. Você pode acessar o portal com seu CPF/CNPJ e a nova senha.
</p> </p>
</div> </div>
<Button label="Ir para o login" icon="pi pi-sign-in" class="w-full" size="large" @click="router.push({ path: '/login', query: { doc: docDigitos } })" /> <Button label="Ir para o login" icon="pi pi-sign-in" class="w-full" size="large" :loading="carregando" @click="entrarKeycloak" />
</template> </template>
</div> </div>