developer #4
@ -1,10 +1,8 @@
|
|||||||
<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)
|
||||||
@ -12,21 +10,12 @@ const erro = ref('')
|
|||||||
|
|
||||||
const documento = ref('')
|
const documento = ref('')
|
||||||
const contribuinteNome = ref('')
|
const contribuinteNome = ref('')
|
||||||
|
const emailMascarado = ref('')
|
||||||
const canais = ref([])
|
|
||||||
const canalSelecionado = ref(null)
|
|
||||||
|
|
||||||
const codigo = ref('')
|
|
||||||
const tokenValidacao = ref('')
|
|
||||||
|
|
||||||
const senha = ref('')
|
|
||||||
const senhaConfirm = ref('')
|
|
||||||
|
|
||||||
const docDigitos = computed(() => documento.value.replace(/\D/g, ''))
|
const docDigitos = computed(() => documento.value.replace(/\D/g, ''))
|
||||||
const docValido = computed(() => docDigitos.value.length === 11 || docDigitos.value.length === 14)
|
const docValido = computed(() => docDigitos.value.length === 11 || docDigitos.value.length === 14)
|
||||||
|
|
||||||
const senhaForte = computed(() => senha.value.length >= 8)
|
const subtitulos = ['Identificação', 'Confirmação', 'Pronto!']
|
||||||
const senhasIguais = computed(() => senha.value === senhaConfirm.value)
|
|
||||||
|
|
||||||
async function identificar() {
|
async function identificar() {
|
||||||
if (!docValido.value) return
|
if (!docValido.value) return
|
||||||
@ -35,81 +24,37 @@ async function identificar() {
|
|||||||
try {
|
try {
|
||||||
const res = await primeiroAcessoService.verificarDocumento(docDigitos.value)
|
const res = await primeiroAcessoService.verificarDocumento(docDigitos.value)
|
||||||
contribuinteNome.value = res.data.nome
|
contribuinteNome.value = res.data.nome
|
||||||
canais.value = res.data.canais
|
const canalEmail = res.data.canais?.find(c => c.tipo === 'EMAIL')
|
||||||
canalSelecionado.value = canais.value[0] ?? null
|
emailMascarado.value = canalEmail?.valor ?? ''
|
||||||
etapa.value = 1
|
etapa.value = 1
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
erro.value = e?.data?.description ?? 'Documento não encontrado ou não habilitado para este fluxo.'
|
erro.value = e?.data?.description ?? 'Documento não encontrado ou não habilitado para acesso ao portal.'
|
||||||
} finally {
|
} finally {
|
||||||
carregando.value = false
|
carregando.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function enviarCodigo() {
|
async function solicitar() {
|
||||||
if (!canalSelecionado.value) return
|
|
||||||
carregando.value = true
|
carregando.value = true
|
||||||
erro.value = ''
|
erro.value = ''
|
||||||
try {
|
try {
|
||||||
await primeiroAcessoService.solicitarCodigo(docDigitos.value, canalSelecionado.value.tipo)
|
await primeiroAcessoService.solicitarAcesso(docDigitos.value)
|
||||||
etapa.value = 2
|
etapa.value = 2
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
erro.value = e?.data?.description ?? 'Não foi possível enviar o código. Tente novamente.'
|
erro.value = e?.data?.description ?? 'Não foi possível solicitar o acesso. Tente novamente.'
|
||||||
} finally {
|
} finally {
|
||||||
carregando.value = false
|
carregando.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function validarCodigo() {
|
|
||||||
if (codigo.value.length < 6) return
|
|
||||||
carregando.value = true
|
|
||||||
erro.value = ''
|
|
||||||
try {
|
|
||||||
const res = await primeiroAcessoService.validarCodigo(docDigitos.value, codigo.value)
|
|
||||||
tokenValidacao.value = res.data.token
|
|
||||||
etapa.value = 3
|
|
||||||
} catch (e) {
|
|
||||||
erro.value = e?.data?.description ?? 'Código inválido ou expirado. Solicite um novo.'
|
|
||||||
} finally {
|
|
||||||
carregando.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function definirSenha() {
|
|
||||||
if (!senhaForte.value || !senhasIguais.value) return
|
|
||||||
carregando.value = true
|
|
||||||
erro.value = ''
|
|
||||||
try {
|
|
||||||
await primeiroAcessoService.definirSenha(tokenValidacao.value, senha.value)
|
|
||||||
etapa.value = 4
|
|
||||||
} catch (e) {
|
|
||||||
erro.value = e?.data?.description ?? 'Não foi possível definir a senha. Tente novamente.'
|
|
||||||
} finally {
|
|
||||||
carregando.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 labelCanal = { EMAIL: 'E-mail', SMS: 'SMS', WHATSAPP: 'WhatsApp' }
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="min-h-[calc(100vh-8rem)] flex items-center justify-center px-4 py-12">
|
<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="w-full max-w-md">
|
||||||
|
|
||||||
<div v-if="etapa < 4" class="flex items-center justify-center gap-2 mb-8">
|
<div v-if="etapa < 2" class="flex items-center justify-center gap-2 mb-8">
|
||||||
<div
|
<div
|
||||||
v-for="i in 4"
|
v-for="i in 2"
|
||||||
:key="i"
|
:key="i"
|
||||||
class="h-1.5 rounded-full transition-all duration-300"
|
class="h-1.5 rounded-full transition-all duration-300"
|
||||||
:class="[
|
:class="[
|
||||||
@ -128,9 +73,7 @@ const labelCanal = { EMAIL: 'E-mail', SMS: 'SMS', WHATSAPP: 'WhatsApp' }
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h1 class="text-white font-bold text-base leading-tight">Primeiro Acesso</h1>
|
<h1 class="text-white font-bold text-base leading-tight">Primeiro Acesso</h1>
|
||||||
<p class="text-white/80 text-xs mt-0.5">
|
<p class="text-white/80 text-xs mt-0.5">{{ subtitulos[etapa] }}</p>
|
||||||
{{ ['Identificação', 'Canal de envio', 'Código de verificação', 'Crie sua senha', 'Pronto!'][etapa] }}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -139,7 +82,7 @@ const labelCanal = { EMAIL: 'E-mail', SMS: 'SMS', WHATSAPP: 'WhatsApp' }
|
|||||||
|
|
||||||
<template v-if="etapa === 0">
|
<template v-if="etapa === 0">
|
||||||
<p class="text-sm text-slate-600 dark:text-slate-300">
|
<p class="text-sm text-slate-600 dark:text-slate-300">
|
||||||
Informe seu CPF ou CNPJ para verificarmos seu cadastro e iniciar a criação de senha.
|
Informe seu CPF ou CNPJ para verificarmos seu cadastro e iniciar a criação do seu acesso ao portal.
|
||||||
</p>
|
</p>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-semibold text-slate-700 dark:text-slate-200 mb-1.5">CPF ou CNPJ</label>
|
<label class="block text-sm font-semibold text-slate-700 dark:text-slate-200 mb-1.5">CPF ou CNPJ</label>
|
||||||
@ -148,102 +91,82 @@ const labelCanal = { EMAIL: 'E-mail', SMS: 'SMS', WHATSAPP: 'WhatsApp' }
|
|||||||
<p v-if="erro" role="alert" class="text-sm text-red-600 dark:text-red-400 flex items-center gap-1.5">
|
<p v-if="erro" role="alert" class="text-sm text-red-600 dark:text-red-400 flex items-center gap-1.5">
|
||||||
<i class="pi pi-exclamation-circle" aria-hidden="true" /> {{ erro }}
|
<i class="pi pi-exclamation-circle" aria-hidden="true" /> {{ erro }}
|
||||||
</p>
|
</p>
|
||||||
<Button label="Continuar" icon="pi pi-arrow-right" icon-pos="right" class="w-full" size="large" :loading="carregando" :disabled="!docValido" @click="identificar" />
|
<Button
|
||||||
|
label="Continuar"
|
||||||
|
icon="pi pi-arrow-right"
|
||||||
|
icon-pos="right"
|
||||||
|
class="w-full"
|
||||||
|
size="large"
|
||||||
|
:loading="carregando"
|
||||||
|
:disabled="!docValido || carregando"
|
||||||
|
@click="identificar"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-else-if="etapa === 1">
|
<template v-else-if="etapa === 1">
|
||||||
<div>
|
<div class="space-y-3">
|
||||||
<p class="text-sm text-slate-600 dark:text-slate-300 mb-1">Olá, <strong class="text-slate-800 dark:text-slate-100">{{ contribuinteNome }}</strong>.</p>
|
|
||||||
<p class="text-sm text-slate-600 dark:text-slate-300">
|
<p class="text-sm text-slate-600 dark:text-slate-300">
|
||||||
Enviaremos um código de verificação. Escolha como prefere receber:
|
Olá, <strong class="text-slate-800 dark:text-slate-100">{{ contribuinteNome }}</strong>.
|
||||||
|
</p>
|
||||||
|
<p class="text-sm text-slate-600 dark:text-slate-300">
|
||||||
|
Vamos criar seu acesso ao portal. Ao confirmar, você receberá um e-mail
|
||||||
|
<template v-if="emailMascarado">
|
||||||
|
para <strong class="text-slate-800 dark:text-slate-100">{{ emailMascarado }}</strong>
|
||||||
|
</template>
|
||||||
|
com um link para definir sua senha.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-2">
|
|
||||||
<button
|
<div class="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-xl p-4 flex gap-3">
|
||||||
v-for="canal in canais"
|
<i class="pi pi-envelope text-blue-600 dark:text-blue-400 mt-0.5 flex-shrink-0" aria-hidden="true" />
|
||||||
:key="canal.tipo"
|
<p class="text-sm text-blue-700 dark:text-blue-300">
|
||||||
class="w-full flex items-center gap-3 p-4 rounded-xl border transition-colors text-left"
|
O link de acesso é enviado pelo Keycloak e expira em 24 horas. Verifique também a caixa de spam.
|
||||||
:class="canalSelecionado?.tipo === canal.tipo
|
</p>
|
||||||
? 'border-primary bg-primary/5 dark:bg-primary/10'
|
|
||||||
: 'border-slate-200 dark:border-slate-600 hover:border-slate-300 dark:hover:border-slate-500'"
|
|
||||||
@click="canalSelecionado = canal"
|
|
||||||
>
|
|
||||||
<div class="w-9 h-9 rounded-lg flex items-center justify-center flex-shrink-0"
|
|
||||||
:class="canalSelecionado?.tipo === canal.tipo ? 'bg-primary text-white' : 'bg-slate-100 dark:bg-slate-700 text-slate-500 dark:text-slate-400'">
|
|
||||||
<i :class="['pi', iconeCanal[canal.tipo] ?? 'pi-send', 'text-sm']" aria-hidden="true" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p class="font-semibold text-slate-800 dark:text-slate-100 text-sm">{{ labelCanal[canal.tipo] ?? canal.tipo }}</p>
|
|
||||||
<p class="text-xs text-slate-500 dark:text-slate-400">{{ canal.valor }}</p>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p v-if="erro" role="alert" class="text-sm text-red-600 dark:text-red-400 flex items-center gap-1.5">
|
<p v-if="erro" role="alert" class="text-sm text-red-600 dark:text-red-400 flex items-center gap-1.5">
|
||||||
<i class="pi pi-exclamation-circle" aria-hidden="true" /> {{ erro }}
|
<i class="pi pi-exclamation-circle" aria-hidden="true" /> {{ erro }}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="flex gap-3">
|
<div class="flex gap-3">
|
||||||
<Button label="Voltar" severity="secondary" outlined class="flex-1" @click="etapa = 0" />
|
<Button label="Voltar" severity="secondary" outlined class="flex-1" :disabled="carregando" @click="etapa = 0" />
|
||||||
<Button label="Enviar código" icon="pi pi-send" class="flex-1" :loading="carregando" :disabled="!canalSelecionado" @click="enviarCodigo" />
|
<Button
|
||||||
|
label="Solicitar acesso"
|
||||||
|
icon="pi pi-send"
|
||||||
|
class="flex-1"
|
||||||
|
:loading="carregando"
|
||||||
|
:disabled="carregando"
|
||||||
|
@click="solicitar"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-else-if="etapa === 2">
|
|
||||||
<p class="text-sm text-slate-600 dark:text-slate-300">
|
|
||||||
Enviamos um código de 6 dígitos para
|
|
||||||
<strong class="text-slate-800 dark:text-slate-100">{{ canalSelecionado?.valor }}</strong>.
|
|
||||||
Ele expira em 10 minutos.
|
|
||||||
</p>
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-semibold text-slate-700 dark:text-slate-200 mb-1.5">Código de verificação</label>
|
|
||||||
<InputOtp v-model="codigo" :length="6" class="justify-center gap-2" integer-only @keyup.enter="validarCodigo" />
|
|
||||||
</div>
|
|
||||||
<p v-if="erro" role="alert" class="text-sm text-red-600 dark:text-red-400 flex items-center gap-1.5">
|
|
||||||
<i class="pi pi-exclamation-circle" aria-hidden="true" /> {{ erro }}
|
|
||||||
</p>
|
|
||||||
<Button label="Verificar código" class="w-full" size="large" :loading="carregando" :disabled="codigo.length < 6" @click="validarCodigo" />
|
|
||||||
<button class="w-full text-center text-sm text-primary hover:underline" @click="enviarCodigo">
|
|
||||||
Não recebi o código — reenviar
|
|
||||||
</button>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template v-else-if="etapa === 3">
|
|
||||||
<p class="text-sm text-slate-600 dark:text-slate-300">
|
|
||||||
Crie uma senha segura com pelo menos 8 caracteres.
|
|
||||||
</p>
|
|
||||||
<div class="space-y-4">
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-semibold text-slate-700 dark:text-slate-200 mb-1.5">Nova senha</label>
|
|
||||||
<Password v-model="senha" :feedback="true" toggle-mask placeholder="Mínimo 8 caracteres" class="w-full" input-class="w-full" size="large" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-semibold text-slate-700 dark:text-slate-200 mb-1.5">Confirmar senha</label>
|
|
||||||
<Password v-model="senhaConfirm" :feedback="false" toggle-mask placeholder="Repita a senha" class="w-full" input-class="w-full" size="large" :invalid="senhaConfirm.length > 0 && !senhasIguais" />
|
|
||||||
<p v-if="senhaConfirm.length > 0 && !senhasIguais" class="mt-1 text-xs text-red-600 dark:text-red-400">As senhas não coincidem.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p v-if="erro" role="alert" class="text-sm text-red-600 dark:text-red-400 flex items-center gap-1.5">
|
|
||||||
<i class="pi pi-exclamation-circle" aria-hidden="true" /> {{ erro }}
|
|
||||||
</p>
|
|
||||||
<Button label="Criar senha" icon="pi pi-check" class="w-full" size="large" :loading="carregando" :disabled="!senhaForte || !senhasIguais" @click="definirSenha" />
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div class="text-center py-4">
|
<div class="text-center py-4">
|
||||||
<div class="w-16 h-16 bg-emerald-100 dark:bg-emerald-900/30 rounded-2xl flex items-center justify-center mx-auto mb-4">
|
<div class="w-16 h-16 bg-emerald-100 dark:bg-emerald-900/30 rounded-2xl flex items-center justify-center mx-auto mb-4">
|
||||||
<i class="pi pi-check-circle text-emerald-600 dark:text-emerald-400 text-3xl" aria-hidden="true" />
|
<i class="pi pi-envelope-open text-emerald-600 dark:text-emerald-400 text-3xl" aria-hidden="true" />
|
||||||
</div>
|
</div>
|
||||||
<h2 class="text-lg font-bold text-slate-800 dark:text-slate-100 mb-2">Senha criada com sucesso!</h2>
|
<h2 class="text-lg font-bold text-slate-800 dark:text-slate-100 mb-2">E-mail enviado!</h2>
|
||||||
<p class="text-sm text-slate-500 dark:text-slate-400">
|
<p class="text-sm text-slate-500 dark:text-slate-400">
|
||||||
Você já pode acessar o portal com seu CPF/CNPJ e a nova senha.
|
Acesse o e-mail cadastrado e clique no link para definir sua senha e acessar o portal.
|
||||||
|
</p>
|
||||||
|
<p v-if="emailMascarado" class="mt-2 text-sm font-medium text-slate-700 dark:text-slate-300">
|
||||||
|
{{ emailMascarado }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Button label="Ir para o login" icon="pi pi-sign-in" class="w-full" size="large" :loading="carregando" @click="entrarKeycloak" />
|
<Button
|
||||||
|
label="Ir para o login"
|
||||||
|
icon="pi pi-sign-in"
|
||||||
|
class="w-full"
|
||||||
|
size="large"
|
||||||
|
@click="router.push('/entrar')"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="etapa < 4" class="text-center mt-4">
|
<div v-if="etapa < 2" class="text-center mt-4">
|
||||||
<button
|
<button
|
||||||
class="inline-flex items-center gap-2 text-sm text-slate-500 dark:text-slate-400 hover:text-slate-700 dark:hover:text-slate-200 transition-colors py-3 px-4 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-800"
|
class="inline-flex items-center gap-2 text-sm text-slate-500 dark:text-slate-400 hover:text-slate-700 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('/')"
|
@click="router.push('/')"
|
||||||
|
|||||||
@ -12,6 +12,15 @@ export const primeiroAcessoService = {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
solicitarAcesso(documento) {
|
||||||
|
return $fetch(proxyUrl('/publico/primeiro-acesso/solicitar'), {
|
||||||
|
method: 'POST',
|
||||||
|
headers: FETCH_HEADERS,
|
||||||
|
body: { documento },
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
// TODO Opção A (futuro — fluxo OTP para verificação de identidade antes de criar conta)
|
||||||
solicitarCodigo(documento, canal) {
|
solicitarCodigo(documento, canal) {
|
||||||
return $fetch(proxyUrl('/publico/primeiro-acesso/codigo'), {
|
return $fetch(proxyUrl('/publico/primeiro-acesso/codigo'), {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user