Correcao Sidebar Classico e Rail, Correcao Layout, Ajuste de Breakpoint para Tailwind, Ajuste AppTopbar, Ajuste Menu PopOver, Recriado Paleta de Cores, Inserido algumas animações leves, Reajuste Cor items NOVOS da tabela, Drawer Ajuda Corrigido no Logout, Whatsapp, sms, email, recursos extras
This commit is contained in:
@@ -15,364 +15,305 @@
|
||||
|--------------------------------------------------------------------------
|
||||
-->
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { ref, computed, onMounted, onBeforeUnmount } from 'vue';
|
||||
|
||||
import Password from 'primevue/password'
|
||||
import Button from 'primevue/button'
|
||||
import { useToast } from 'primevue/usetoast'
|
||||
import Password from 'primevue/password';
|
||||
import Button from 'primevue/button';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
|
||||
import { supabase } from '@/lib/supabase/client'
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
|
||||
const toast = useToast()
|
||||
const toast = useToast();
|
||||
|
||||
// ── Hero sticky ────────────────────────────────────────────
|
||||
const headerEl = ref(null)
|
||||
const headerSentinelRef = ref(null)
|
||||
const headerStuck = ref(false)
|
||||
let _observer = null
|
||||
const headerEl = ref(null);
|
||||
const headerSentinelRef = ref(null);
|
||||
const headerStuck = ref(false);
|
||||
let _observer = null;
|
||||
|
||||
const currentPassword = ref('')
|
||||
const newPassword = ref('')
|
||||
const confirmPassword = ref('')
|
||||
const loading = ref(false)
|
||||
const loadingReset = ref(false)
|
||||
const mounted = ref(false)
|
||||
const done = ref(false)
|
||||
const currentPassword = ref('');
|
||||
const newPassword = ref('');
|
||||
const confirmPassword = ref('');
|
||||
const loading = ref(false);
|
||||
const loadingReset = ref(false);
|
||||
const mounted = ref(false);
|
||||
const done = ref(false);
|
||||
|
||||
// ── validações ────────────────────────────────────────────────────────────
|
||||
|
||||
function isStrongEnough (p) {
|
||||
return /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$/.test(p || '')
|
||||
function isStrongEnough(p) {
|
||||
return /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$/.test(p || '');
|
||||
}
|
||||
|
||||
const strengthScore = computed(() => {
|
||||
const p = newPassword.value
|
||||
if (!p) return 0
|
||||
let s = 0
|
||||
if (p.length >= 8) s++
|
||||
if (/[A-Z]/.test(p)) s++
|
||||
if (/[a-z]/.test(p)) s++
|
||||
if (/\d/.test(p)) s++
|
||||
return s
|
||||
})
|
||||
const p = newPassword.value;
|
||||
if (!p) return 0;
|
||||
let s = 0;
|
||||
if (p.length >= 8) s++;
|
||||
if (/[A-Z]/.test(p)) s++;
|
||||
if (/[a-z]/.test(p)) s++;
|
||||
if (/\d/.test(p)) s++;
|
||||
return s;
|
||||
});
|
||||
|
||||
const strengthLabel = computed(() => {
|
||||
const labels = ['', 'Muito fraca', 'Fraca', 'Boa', 'Forte']
|
||||
return labels[strengthScore.value] || ''
|
||||
})
|
||||
const labels = ['', 'Muito fraca', 'Fraca', 'Boa', 'Forte'];
|
||||
return labels[strengthScore.value] || '';
|
||||
});
|
||||
|
||||
const strengthColor = computed(() => {
|
||||
const colors = ['', 'bg-red-500', 'bg-yellow-500', 'bg-blue-500', 'bg-emerald-500']
|
||||
return colors[strengthScore.value] || ''
|
||||
})
|
||||
const colors = ['', 'bg-red-500', 'bg-yellow-500', 'bg-blue-500', 'bg-emerald-500'];
|
||||
return colors[strengthScore.value] || '';
|
||||
});
|
||||
|
||||
const strengthTextColor = computed(() => {
|
||||
const colors = ['', 'text-red-500', 'text-yellow-500', 'text-blue-500', 'text-emerald-500']
|
||||
return colors[strengthScore.value] || ''
|
||||
})
|
||||
const colors = ['', 'text-red-500', 'text-yellow-500', 'text-blue-500', 'text-emerald-500'];
|
||||
return colors[strengthScore.value] || '';
|
||||
});
|
||||
|
||||
const strengthOk = computed(() => isStrongEnough(newPassword.value))
|
||||
const matchOk = computed(() => !!confirmPassword.value && newPassword.value === confirmPassword.value)
|
||||
const strengthOk = computed(() => isStrongEnough(newPassword.value));
|
||||
const matchOk = computed(() => !!confirmPassword.value && newPassword.value === confirmPassword.value);
|
||||
|
||||
const canSubmit = computed(() =>
|
||||
!!currentPassword.value && strengthOk.value && matchOk.value && !loading.value && !loadingReset.value
|
||||
)
|
||||
const canSubmit = computed(() => !!currentPassword.value && strengthOk.value && matchOk.value && !loading.value && !loadingReset.value);
|
||||
|
||||
// ── ações ─────────────────────────────────────────────────────────────────
|
||||
|
||||
function clearFields () {
|
||||
currentPassword.value = ''
|
||||
newPassword.value = ''
|
||||
confirmPassword.value = ''
|
||||
function clearFields() {
|
||||
currentPassword.value = '';
|
||||
newPassword.value = '';
|
||||
confirmPassword.value = '';
|
||||
}
|
||||
|
||||
async function hardLogout () {
|
||||
try { await supabase.auth.signOut({ scope: 'global' }) } catch {}
|
||||
try {
|
||||
const keys = []
|
||||
for (let i = 0; i < localStorage.length; i++) {
|
||||
const k = localStorage.key(i)
|
||||
if (k?.startsWith('sb-') && k.includes('auth-token')) keys.push(k)
|
||||
async function hardLogout() {
|
||||
try {
|
||||
await supabase.auth.signOut({ scope: 'global' });
|
||||
} catch {}
|
||||
try {
|
||||
const keys = [];
|
||||
for (let i = 0; i < localStorage.length; i++) {
|
||||
const k = localStorage.key(i);
|
||||
if (k?.startsWith('sb-') && k.includes('auth-token')) keys.push(k);
|
||||
}
|
||||
keys.forEach((k) => localStorage.removeItem(k));
|
||||
} catch {}
|
||||
try {
|
||||
sessionStorage.removeItem('redirect_after_login');
|
||||
} catch {}
|
||||
window.location.replace('/auth/login');
|
||||
}
|
||||
|
||||
async function changePassword() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const { data: uData, error: uErr } = await supabase.auth.getUser();
|
||||
if (uErr) throw uErr;
|
||||
|
||||
const email = uData?.user?.email;
|
||||
if (!email) throw new Error('Sessão inválida. Faça login novamente.');
|
||||
|
||||
// reautentica para confirmar senha atual
|
||||
const { error: signError } = await supabase.auth.signInWithPassword({ email, password: currentPassword.value });
|
||||
if (signError) throw new Error('Senha atual incorreta.');
|
||||
|
||||
// atualiza
|
||||
const { error: upError } = await supabase.auth.updateUser({ password: newPassword.value });
|
||||
if (upError) throw upError;
|
||||
|
||||
clearFields();
|
||||
done.value = true;
|
||||
|
||||
toast.add({ severity: 'success', summary: 'Senha atualizada', detail: 'Por segurança, você será deslogado.', life: 2500 });
|
||||
setTimeout(() => hardLogout(), 2600);
|
||||
} catch (e) {
|
||||
toast.add({ severity: 'error', summary: 'Erro', detail: e?.message || 'Não foi possível trocar a senha.', life: 4000 });
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
keys.forEach(k => localStorage.removeItem(k))
|
||||
} catch {}
|
||||
try { sessionStorage.removeItem('redirect_after_login') } catch {}
|
||||
window.location.replace('/auth/login')
|
||||
}
|
||||
|
||||
async function changePassword () {
|
||||
loading.value = true
|
||||
try {
|
||||
const { data: uData, error: uErr } = await supabase.auth.getUser()
|
||||
if (uErr) throw uErr
|
||||
|
||||
const email = uData?.user?.email
|
||||
if (!email) throw new Error('Sessão inválida. Faça login novamente.')
|
||||
|
||||
// reautentica para confirmar senha atual
|
||||
const { error: signError } = await supabase.auth.signInWithPassword({ email, password: currentPassword.value })
|
||||
if (signError) throw new Error('Senha atual incorreta.')
|
||||
|
||||
// atualiza
|
||||
const { error: upError } = await supabase.auth.updateUser({ password: newPassword.value })
|
||||
if (upError) throw upError
|
||||
|
||||
clearFields()
|
||||
done.value = true
|
||||
|
||||
toast.add({ severity: 'success', summary: 'Senha atualizada', detail: 'Por segurança, você será deslogado.', life: 2500 })
|
||||
setTimeout(() => hardLogout(), 2600)
|
||||
} catch (e) {
|
||||
toast.add({ severity: 'error', summary: 'Erro', detail: e?.message || 'Não foi possível trocar a senha.', life: 4000 })
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const rootMargin = `${document.querySelector('.l2-main') ? '0px' : '-56px'} 0px 0px 0px`
|
||||
_observer = new IntersectionObserver(
|
||||
([entry]) => { headerStuck.value = !entry.isIntersecting },
|
||||
{ threshold: 0, rootMargin }
|
||||
)
|
||||
if (headerSentinelRef.value) _observer.observe(headerSentinelRef.value)
|
||||
mounted.value = true
|
||||
})
|
||||
const rootMargin = `${document.querySelector('.l2-main') ? '0px' : '-56px'} 0px 0px 0px`;
|
||||
_observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
headerStuck.value = !entry.isIntersecting;
|
||||
},
|
||||
{ threshold: 0, rootMargin }
|
||||
);
|
||||
if (headerSentinelRef.value) _observer.observe(headerSentinelRef.value);
|
||||
mounted.value = true;
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => { _observer?.disconnect() })
|
||||
onBeforeUnmount(() => {
|
||||
_observer?.disconnect();
|
||||
});
|
||||
|
||||
async function sendResetEmail () {
|
||||
loadingReset.value = true
|
||||
try {
|
||||
const { data: uData, error: uErr } = await supabase.auth.getUser()
|
||||
if (uErr) throw uErr
|
||||
async function sendResetEmail() {
|
||||
loadingReset.value = true;
|
||||
try {
|
||||
const { data: uData, error: uErr } = await supabase.auth.getUser();
|
||||
if (uErr) throw uErr;
|
||||
|
||||
const email = uData?.user?.email
|
||||
if (!email) throw new Error('Sessão inválida. Faça login novamente.')
|
||||
const email = uData?.user?.email;
|
||||
if (!email) throw new Error('Sessão inválida. Faça login novamente.');
|
||||
|
||||
const redirectTo = `${window.location.origin}/auth/reset-password`
|
||||
const { error } = await supabase.auth.resetPasswordForEmail(email, { redirectTo })
|
||||
if (error) throw error
|
||||
const redirectTo = `${window.location.origin}/auth/reset-password`;
|
||||
const { error } = await supabase.auth.resetPasswordForEmail(email, { redirectTo });
|
||||
if (error) throw error;
|
||||
|
||||
toast.add({ severity: 'info', summary: 'E-mail enviado', detail: 'Verifique sua caixa de entrada para redefinir a senha.', life: 5000 })
|
||||
} catch (e) {
|
||||
toast.add({ severity: 'error', summary: 'Erro', detail: e?.message || 'Falha ao enviar e-mail.', life: 4500 })
|
||||
} finally {
|
||||
loadingReset.value = false
|
||||
}
|
||||
toast.add({ severity: 'info', summary: 'E-mail enviado', detail: 'Verifique sua caixa de entrada para redefinir a senha.', life: 5000 });
|
||||
} catch (e) {
|
||||
toast.add({ severity: 'error', summary: 'Erro', detail: e?.message || 'Falha ao enviar e-mail.', life: 4500 });
|
||||
} finally {
|
||||
loadingReset.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- Sentinel -->
|
||||
<div ref="headerSentinelRef" class="h-px" />
|
||||
|
||||
<!-- Sentinel -->
|
||||
<div ref="headerSentinelRef" class="h-px" />
|
||||
|
||||
<!-- Hero sticky -->
|
||||
<div
|
||||
ref="headerEl"
|
||||
class="sticky mx-auto max-w-2xl px-3 md:px-5 mb-4 z-20 overflow-hidden rounded-md border border-[var(--surface-border)] bg-[var(--surface-card)] p-5"
|
||||
:style="{ top: 'var(--layout-sticky-top, 56px)' }"
|
||||
:class="{ 'rounded-tl-none rounded-tr-none': headerStuck }"
|
||||
>
|
||||
<!-- Blobs -->
|
||||
<div class="absolute inset-0 pointer-events-none overflow-hidden" aria-hidden="true">
|
||||
<div class="absolute rounded-full blur-[70px] w-72 h-72 -top-16 -right-12 bg-indigo-500/10" />
|
||||
<div class="absolute rounded-full blur-[70px] w-80 h-80 top-2 -left-20 bg-emerald-500/[0.08]" />
|
||||
</div>
|
||||
|
||||
<div class="relative z-10 flex items-center gap-4">
|
||||
<div class="flex items-center gap-3 flex-1 min-w-0">
|
||||
<div
|
||||
class="grid place-items-center w-10 h-10 rounded-[0.875rem] shrink-0"
|
||||
style="background: color-mix(in srgb, var(--p-primary-500, #6366f1) 12%, transparent); color: var(--p-primary-500, #6366f1)"
|
||||
>
|
||||
<i class="pi pi-shield text-lg" />
|
||||
<!-- Hero sticky -->
|
||||
<div
|
||||
ref="headerEl"
|
||||
class="sticky mx-auto max-w-2xl px-3 md:px-5 mb-4 z-20 overflow-hidden rounded-md border border-[var(--surface-border)] bg-[var(--surface-card)] p-5"
|
||||
:style="{ top: 'var(--layout-sticky-top, 56px)' }"
|
||||
:class="{ 'rounded-tl-none rounded-tr-none': headerStuck }"
|
||||
>
|
||||
<!-- Blobs -->
|
||||
<div class="absolute inset-0 pointer-events-none overflow-hidden" aria-hidden="true">
|
||||
<div class="absolute rounded-full blur-[70px] w-72 h-72 -top-16 -right-12 bg-indigo-500/10" />
|
||||
<div class="absolute rounded-full blur-[70px] w-80 h-80 top-2 -left-20 bg-emerald-500/[0.08]" />
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<div class="text-[1rem] font-bold tracking-tight text-[var(--text-color)]">Segurança</div>
|
||||
<div class="text-[1rem] text-[var(--text-color-secondary)] mt-0.5">Gerencie o acesso e a senha da sua conta</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span class="hidden xl:inline-flex items-center gap-2 text-[1rem] px-3 py-1.5 rounded-full border border-emerald-200 text-emerald-700 bg-emerald-50 shrink-0">
|
||||
<span class="h-2 w-2 rounded-full bg-emerald-500 animate-pulse" />
|
||||
Sessão ativa
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="px-3 md:px-5 pb-8 flex justify-center">
|
||||
<div class="w-full max-w-2xl space-y-4">
|
||||
|
||||
<!-- Card principal -->
|
||||
<div class="rounded-md border border-[var(--surface-border)] bg-[var(--surface-card)] overflow-hidden">
|
||||
|
||||
<!-- Seção: Trocar senha -->
|
||||
<div class="px-6 py-5 border-b border-[var(--surface-border)]">
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<div class="text-[1rem] font-semibold text-[var(--text-color)]">Trocar senha</div>
|
||||
<div class="text-[1rem] text-[var(--text-color-secondary)] mt-0.5">
|
||||
Confirme sua senha atual e defina uma nova.
|
||||
</div>
|
||||
<div class="relative z-10 flex items-center gap-4">
|
||||
<div class="flex items-center gap-3 flex-1 min-w-0">
|
||||
<div class="grid place-items-center w-10 h-10 rounded-[0.875rem] shrink-0" style="background: color-mix(in srgb, var(--p-primary-500, #6366f1) 12%, transparent); color: var(--p-primary-500, #6366f1)">
|
||||
<i class="pi pi-shield text-lg" />
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<div class="text-[1rem] font-bold tracking-tight text-[var(--text-color)]">Segurança</div>
|
||||
<div class="text-[1rem] text-[var(--text-color-secondary)] mt-0.5">Gerencie o acesso e a senha da sua conta</div>
|
||||
</div>
|
||||
</div>
|
||||
<span class="hidden sm:inline-flex items-center gap-1.5 rounded-full border border-[var(--surface-border)] bg-[var(--surface-ground)] px-3 py-1 text-[1rem] text-[var(--text-color-secondary)]">
|
||||
<span class="h-1.5 w-1.5 rounded-full bg-emerald-400" />
|
||||
sessão ativa
|
||||
|
||||
<span class="hidden xl:inline-flex items-center gap-2 text-[1rem] px-3 py-1.5 rounded-full border border-emerald-200 text-emerald-700 bg-emerald-50 shrink-0">
|
||||
<span class="h-2 w-2 rounded-full bg-emerald-500 animate-pulse" />
|
||||
Sessão ativa
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Estado: concluído -->
|
||||
<div v-if="done" class="px-6 py-10 text-center space-y-4">
|
||||
<div class="mx-auto grid h-16 w-16 place-items-center rounded-full bg-emerald-500/10 border border-emerald-500/20">
|
||||
<i class="pi pi-check text-emerald-500 text-2xl" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-semibold text-[var(--text-color)]">Senha atualizada!</div>
|
||||
<div class="text-[1rem] text-[var(--text-color-secondary)] mt-1">Redirecionando para o login…</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Estado: formulário -->
|
||||
<div v-else class="px-6 py-6 space-y-5">
|
||||
|
||||
<!-- Senha atual -->
|
||||
<div>
|
||||
<div class="text-[1rem] font-semibold text-[var(--text-color)] mb-1.5">Senha atual</div>
|
||||
<Password
|
||||
v-model="currentPassword"
|
||||
placeholder="Digite sua senha atual"
|
||||
toggleMask
|
||||
:feedback="false"
|
||||
class="w-full"
|
||||
inputClass="w-full"
|
||||
:disabled="loading || loadingReset"
|
||||
/>
|
||||
<div class="mt-1.5 text-[1rem] text-[var(--text-color-secondary)]">
|
||||
Necessária para confirmar que é você.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="h-px bg-[var(--surface-border)]" />
|
||||
|
||||
<!-- Nova senha -->
|
||||
<div>
|
||||
<div class="text-[1rem] font-semibold text-[var(--text-color)] mb-1.5">Nova senha</div>
|
||||
<Password
|
||||
v-model="newPassword"
|
||||
placeholder="Mínimo 8 caracteres"
|
||||
toggleMask
|
||||
:feedback="false"
|
||||
class="w-full"
|
||||
inputClass="w-full"
|
||||
:disabled="loading || loadingReset"
|
||||
/>
|
||||
|
||||
<!-- Barra de força -->
|
||||
<div v-if="newPassword" class="mt-2 space-y-1">
|
||||
<div class="flex gap-1">
|
||||
<div
|
||||
v-for="i in 4"
|
||||
:key="i"
|
||||
class="h-1 flex-1 rounded-full transition-all duration-300"
|
||||
:class="i <= strengthScore ? strengthColor : 'bg-[var(--surface-border)]'"
|
||||
/>
|
||||
</div>
|
||||
<span class="text-[1rem]" :class="strengthTextColor">{{ strengthLabel }}</span>
|
||||
</div>
|
||||
<div v-else class="mt-1.5 text-[1rem] text-[var(--text-color-secondary)]">
|
||||
Critérios: 8+ caracteres, maiúscula, minúscula e número.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Confirmar senha -->
|
||||
<div>
|
||||
<div class="text-[1rem] font-semibold text-[var(--text-color)] mb-1.5">Confirmar nova senha</div>
|
||||
<Password
|
||||
v-model="confirmPassword"
|
||||
placeholder="Repita a nova senha"
|
||||
toggleMask
|
||||
:feedback="false"
|
||||
class="w-full"
|
||||
inputClass="w-full"
|
||||
:disabled="loading || loadingReset"
|
||||
/>
|
||||
<div
|
||||
v-if="confirmPassword"
|
||||
class="mt-1.5 flex items-center gap-1.5 text-[1rem]"
|
||||
:class="matchOk ? 'text-emerald-500' : 'text-yellow-500'"
|
||||
>
|
||||
<i :class="matchOk ? 'pi pi-check' : 'pi pi-times'" />
|
||||
{{ matchOk ? 'As senhas conferem.' : 'As senhas não conferem.' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Aviso -->
|
||||
<div class="rounded-md border border-[var(--surface-border)] bg-[var(--surface-ground)] px-4 py-3 flex items-start gap-3">
|
||||
<i class="pi pi-info-circle text-[var(--text-color-secondary)] mt-0.5 flex-shrink-0" />
|
||||
<div class="text-[1rem] text-[var(--text-color-secondary)] leading-relaxed">
|
||||
Ao trocar sua senha, você será desconectado de todos os dispositivos por segurança.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Ações -->
|
||||
<div class="flex flex-col-reverse gap-2 sm:flex-row sm:justify-between sm:items-center pt-1">
|
||||
<Button
|
||||
label="Enviar link por e-mail"
|
||||
severity="secondary"
|
||||
outlined
|
||||
icon="pi pi-envelope"
|
||||
:loading="loadingReset"
|
||||
:disabled="loading || loadingReset"
|
||||
@click="sendResetEmail"
|
||||
/>
|
||||
<Button
|
||||
label="Trocar senha"
|
||||
icon="pi pi-check"
|
||||
:loading="loading"
|
||||
:disabled="!canSubmit"
|
||||
@click="changePassword"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Card informativo: dicas -->
|
||||
<div class="mt-4 rounded-md border border-[var(--surface-border)] bg-[var(--surface-card)] px-6 py-5">
|
||||
<div class="flex items-center gap-2 mb-3">
|
||||
<i class="pi pi-lightbulb text-[var(--text-color-secondary)]" />
|
||||
<span class="text-[1rem] font-semibold text-[var(--text-color)]">Boas práticas</span>
|
||||
</div>
|
||||
<ul class="space-y-2">
|
||||
<li class="flex items-start gap-2.5 text-[1rem] text-[var(--text-color-secondary)]">
|
||||
<span class="mt-1 h-1.5 w-1.5 rounded-full bg-indigo-400 flex-shrink-0" />
|
||||
Use pelo menos 8 caracteres com maiúscula, minúscula e número.
|
||||
</li>
|
||||
<li class="flex items-start gap-2.5 text-[1rem] text-[var(--text-color-secondary)]">
|
||||
<span class="mt-1 h-1.5 w-1.5 rounded-full bg-emerald-400 flex-shrink-0" />
|
||||
Evite datas, nomes e sequências óbvias (1234, qwerty).
|
||||
</li>
|
||||
<li class="flex items-start gap-2.5 text-[1rem] text-[var(--text-color-secondary)]">
|
||||
<span class="mt-1 h-1.5 w-1.5 rounded-full bg-fuchsia-400 flex-shrink-0" />
|
||||
Se estiver em computador compartilhado, encerre a sessão depois.
|
||||
</li>
|
||||
<li class="flex items-start gap-2.5 text-[1rem] text-[var(--text-color-secondary)]">
|
||||
<span class="mt-1 h-1.5 w-1.5 rounded-full bg-amber-400 flex-shrink-0" />
|
||||
Não reutilize a mesma senha de outros serviços.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="px-3 md:px-4 pb-3">
|
||||
<LoadedPhraseBlock v-if="mounted" />
|
||||
</div>
|
||||
<div class="px-3 md:px-5 pb-8 flex justify-center">
|
||||
<div class="w-full max-w-2xl space-y-4">
|
||||
<!-- Card principal -->
|
||||
<div class="rounded-md border border-[var(--surface-border)] bg-[var(--surface-card)] overflow-hidden">
|
||||
<!-- Seção: Trocar senha -->
|
||||
<div class="px-6 py-5 border-b border-[var(--surface-border)]">
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<div class="text-[1rem] font-semibold text-[var(--text-color)]">Trocar senha</div>
|
||||
<div class="text-[1rem] text-[var(--text-color-secondary)] mt-0.5">Confirme sua senha atual e defina uma nova.</div>
|
||||
</div>
|
||||
<span class="hidden sm:inline-flex items-center gap-1.5 rounded-full border border-[var(--surface-border)] bg-[var(--surface-ground)] px-3 py-1 text-[1rem] text-[var(--text-color-secondary)]">
|
||||
<span class="h-1.5 w-1.5 rounded-full bg-emerald-400" />
|
||||
sessão ativa
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Estado: concluído -->
|
||||
<div v-if="done" class="px-6 py-10 text-center space-y-4">
|
||||
<div class="mx-auto grid h-16 w-16 place-items-center rounded-full bg-emerald-500/10 border border-emerald-500/20">
|
||||
<i class="pi pi-check text-emerald-500 text-2xl" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-semibold text-[var(--text-color)]">Senha atualizada!</div>
|
||||
<div class="text-[1rem] text-[var(--text-color-secondary)] mt-1">Redirecionando para o login…</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Estado: formulário -->
|
||||
<div v-else class="px-6 py-6 space-y-5">
|
||||
<!-- Senha atual -->
|
||||
<div>
|
||||
<div class="text-[1rem] font-semibold text-[var(--text-color)] mb-1.5">Senha atual</div>
|
||||
<Password v-model="currentPassword" placeholder="Digite sua senha atual" toggleMask :feedback="false" class="w-full" inputClass="w-full" :disabled="loading || loadingReset" />
|
||||
<div class="mt-1.5 text-[1rem] text-[var(--text-color-secondary)]">Necessária para confirmar que é você.</div>
|
||||
</div>
|
||||
|
||||
<div class="h-px bg-[var(--surface-border)]" />
|
||||
|
||||
<!-- Nova senha -->
|
||||
<div>
|
||||
<div class="text-[1rem] font-semibold text-[var(--text-color)] mb-1.5">Nova senha</div>
|
||||
<Password v-model="newPassword" placeholder="Mínimo 8 caracteres" toggleMask :feedback="false" class="w-full" inputClass="w-full" :disabled="loading || loadingReset" />
|
||||
|
||||
<!-- Barra de força -->
|
||||
<div v-if="newPassword" class="mt-2 space-y-1">
|
||||
<div class="flex gap-1">
|
||||
<div v-for="i in 4" :key="i" class="h-1 flex-1 rounded-full transition-all duration-300" :class="i <= strengthScore ? strengthColor : 'bg-[var(--surface-border)]'" />
|
||||
</div>
|
||||
<span class="text-[1rem]" :class="strengthTextColor">{{ strengthLabel }}</span>
|
||||
</div>
|
||||
<div v-else class="mt-1.5 text-[1rem] text-[var(--text-color-secondary)]">Critérios: 8+ caracteres, maiúscula, minúscula e número.</div>
|
||||
</div>
|
||||
|
||||
<!-- Confirmar senha -->
|
||||
<div>
|
||||
<div class="text-[1rem] font-semibold text-[var(--text-color)] mb-1.5">Confirmar nova senha</div>
|
||||
<Password v-model="confirmPassword" placeholder="Repita a nova senha" toggleMask :feedback="false" class="w-full" inputClass="w-full" :disabled="loading || loadingReset" />
|
||||
<div v-if="confirmPassword" class="mt-1.5 flex items-center gap-1.5 text-[1rem]" :class="matchOk ? 'text-emerald-500' : 'text-yellow-500'">
|
||||
<i :class="matchOk ? 'pi pi-check' : 'pi pi-times'" />
|
||||
{{ matchOk ? 'As senhas conferem.' : 'As senhas não conferem.' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Aviso -->
|
||||
<div class="rounded-md border border-[var(--surface-border)] bg-[var(--surface-ground)] px-4 py-3 flex items-start gap-3">
|
||||
<i class="pi pi-info-circle text-[var(--text-color-secondary)] mt-0.5 flex-shrink-0" />
|
||||
<div class="text-[1rem] text-[var(--text-color-secondary)] leading-relaxed">Ao trocar sua senha, você será desconectado de todos os dispositivos por segurança.</div>
|
||||
</div>
|
||||
|
||||
<!-- Ações -->
|
||||
<div class="flex flex-col-reverse gap-2 sm:flex-row sm:justify-between sm:items-center pt-1">
|
||||
<Button label="Enviar link por e-mail" severity="secondary" outlined icon="pi pi-envelope" :loading="loadingReset" :disabled="loading || loadingReset" @click="sendResetEmail" />
|
||||
<Button label="Trocar senha" icon="pi pi-check" :loading="loading" :disabled="!canSubmit" @click="changePassword" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Card informativo: dicas -->
|
||||
<div class="mt-4 rounded-md border border-[var(--surface-border)] bg-[var(--surface-card)] px-6 py-5">
|
||||
<div class="flex items-center gap-2 mb-3">
|
||||
<i class="pi pi-lightbulb text-[var(--text-color-secondary)]" />
|
||||
<span class="text-[1rem] font-semibold text-[var(--text-color)]">Boas práticas</span>
|
||||
</div>
|
||||
<ul class="space-y-2">
|
||||
<li class="flex items-start gap-2.5 text-[1rem] text-[var(--text-color-secondary)]">
|
||||
<span class="mt-1 h-1.5 w-1.5 rounded-full bg-indigo-400 flex-shrink-0" />
|
||||
Use pelo menos 8 caracteres com maiúscula, minúscula e número.
|
||||
</li>
|
||||
<li class="flex items-start gap-2.5 text-[1rem] text-[var(--text-color-secondary)]">
|
||||
<span class="mt-1 h-1.5 w-1.5 rounded-full bg-emerald-400 flex-shrink-0" />
|
||||
Evite datas, nomes e sequências óbvias (1234, qwerty).
|
||||
</li>
|
||||
<li class="flex items-start gap-2.5 text-[1rem] text-[var(--text-color-secondary)]">
|
||||
<span class="mt-1 h-1.5 w-1.5 rounded-full bg-fuchsia-400 flex-shrink-0" />
|
||||
Se estiver em computador compartilhado, encerre a sessão depois.
|
||||
</li>
|
||||
<li class="flex items-start gap-2.5 text-[1rem] text-[var(--text-color-secondary)]">
|
||||
<span class="mt-1 h-1.5 w-1.5 rounded-full bg-amber-400 flex-shrink-0" />
|
||||
Não reutilize a mesma senha de outros serviços.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="px-3 md:px-4 pb-3">
|
||||
<LoadedPhraseBlock v-if="mounted" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
<style scoped></style>
|
||||
|
||||
Reference in New Issue
Block a user