MelissaSeguranca: pagina nativa 2-col + grupo "Conta" sai inteiro de Configuracoes
Tira "Seguranca" do MelissaConfiguracoes (era embed cfg-seguranca -> SecurityPage.vue) e cria a /melissa/seguranca nativa Melissa. Sidebar (mse-side): - Card "Estado da conta" — email (mono) + tag "Sessão Ativa" pulsando + warning amber sobre desconectar todos dispositivos - Card "Boas praticas" — 4 dicas com bullet colorido (8+ chars, evite obvio, encerre sessao publica, nao reuse senhas) Main: - Card "Trocar senha" — 3 Password inputs (atual + nova + confirmar) + barra de forca 4-segmentos (Muito fraca/Fraca/Boa/Forte) + match indicator (check verde / x amber) + warning + 2 botoes: "Enviar link por e-mail" (reset por email) + primary "Trocar senha" - Estado "concluido" com check verde + redirect pro login Logica espelhada do SecurityPage: - changePassword: signInWithPassword pra reautenticar + updateUser + hardLogout (signOut global + clear sb-* tokens) apos 2.6s - sendResetEmail: resetPasswordForEmail com redirectTo /auth/reset-password Wire-up: - MelissaLayout: import + render `<MelissaSeguranca v-if=secaoAberta=='seguranca'>` - 'seguranca' sai de MELISSA_CONFIG_ALIASES, entra em MELISSA_NON_CONFIG_SLUGS - SECOES.seguranca atualizado (label + descricao + duplicado removido) - MelissaConfiguracoes: cfg-seguranca removido de COMPONENT_MAP + ROUTE_ALIASES; grupo "Conta" inteiro removido (Perfil/Plano/Negocio/ Seguranca todos viraram nativos agora) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -39,7 +39,6 @@ const emit = defineEmits(['close']);
|
||||
// existir, senão a key como-é.
|
||||
const ROUTE_ALIASES = {
|
||||
aparencia: 'aparencia',
|
||||
seguranca: 'cfg-seguranca',
|
||||
bloqueios: 'cfg-bloqueios'
|
||||
};
|
||||
// URLs antigas (/melissa/fundo, /melissa/relogio, /melissa/cronometro)
|
||||
@@ -96,10 +95,10 @@ const COMPONENT_MAP = {
|
||||
'cfg-recursos-extras': defineAsyncComponent(() => import('@/layout/configuracoes/ConfiguracoesRecursosExtrasPage.vue')),
|
||||
'cfg-auditoria': defineAsyncComponent(() => import('@/layout/configuracoes/AuditoriaPage.vue')),
|
||||
// Conta (páginas pessoais que vivem em /account/*)
|
||||
// 'cfg-perfil' removido — virou pagina nativa MelissaPerfil em /melissa/perfil
|
||||
// 'cfg-plano' removido — virou pagina nativa MelissaPlano em /melissa/plano
|
||||
// 'cfg-negocio' removido — virou pagina nativa MelissaNegocio em /melissa/negocio
|
||||
'cfg-seguranca': defineAsyncComponent(() => import('@/views/pages/auth/SecurityPage.vue'))
|
||||
// 'cfg-perfil' removido — virou pagina nativa MelissaPerfil em /melissa/perfil
|
||||
// 'cfg-plano' removido — virou pagina nativa MelissaPlano em /melissa/plano
|
||||
// 'cfg-negocio' removido — virou pagina nativa MelissaNegocio em /melissa/negocio
|
||||
// 'cfg-seguranca' removido — virou pagina nativa MelissaSeguranca em /melissa/seguranca
|
||||
};
|
||||
|
||||
// Keys que renderizam controles inline definidos neste arquivo (Layout Melissa).
|
||||
@@ -145,18 +144,8 @@ const grupos = [
|
||||
{ key: 'aparencia', label: 'Layout Melissa', desc: 'Tema, cor primária, surface, plano de fundo, relógio e cronômetro — tudo numa tela só.', icon: 'pi pi-palette' }
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'conta',
|
||||
label: 'Conta',
|
||||
desc: 'Segurança da sua conta.',
|
||||
icon: 'pi pi-user',
|
||||
items: [
|
||||
// "Meu Perfil" virou pagina nativa em /melissa/perfil (MelissaPerfil)
|
||||
// "Meu Plano" virou pagina nativa em /melissa/plano (MelissaPlano)
|
||||
// "Meu Negócio" virou pagina nativa em /melissa/negocio (MelissaNegocio)
|
||||
{ key: 'cfg-seguranca', label: 'Segurança', desc: 'Senha, dispositivos confiáveis e sessões ativas.', icon: 'pi pi-shield' }
|
||||
]
|
||||
},
|
||||
// Grupo "Conta" inteiro saiu — todos os 4 items (Perfil/Plano/
|
||||
// Negocio/Seguranca) viraram paginas nativas no /melissa/<slug>.
|
||||
{
|
||||
key: 'agenda',
|
||||
label: 'Agenda',
|
||||
|
||||
@@ -37,6 +37,7 @@ import MelissaPerfil from './MelissaPerfil.vue';
|
||||
import MelissaPlano from './MelissaPlano.vue';
|
||||
import MelissaNegocio from './MelissaNegocio.vue';
|
||||
import MelissaAlterarPlano from './MelissaAlterarPlano.vue';
|
||||
import MelissaSeguranca from './MelissaSeguranca.vue';
|
||||
import MelissaEmbed from './MelissaEmbed.vue';
|
||||
import MelissaCadastrosRecebidos from './MelissaCadastrosRecebidos.vue';
|
||||
import MelissaAgendamentosRecebidos from './MelissaAgendamentosRecebidos.vue';
|
||||
@@ -173,9 +174,10 @@ const SECOES = {
|
||||
plano: { label: 'Meu Plano', icon: 'pi pi-credit-card', descricao: 'Assinatura, recursos liberados e historico de mudancas.' },
|
||||
// Pagina nativa do negocio (MelissaNegocio) — saiu do MelissaConfiguracoes
|
||||
negocio: { label: 'Meu Negócio', icon: 'pi pi-briefcase', descricao: 'Identidade, fiscal, endereco, contato, redes — gamificacao no aside.' },
|
||||
// Pagina nativa de seguranca (MelissaSeguranca) — saiu do MelissaConfiguracoes
|
||||
seguranca: { label: 'Segurança', icon: 'pi pi-shield', descricao: 'Trocar senha + boas praticas + estado da sessao.' },
|
||||
// Pagina nativa de alterar plano (MelissaAlterarPlano) — substitui /therapist/upgrade
|
||||
'alterar-plano': { label: 'Alterar Plano', icon: 'pi pi-arrow-up-right', descricao: 'Escolha um plano pessoal pra ativar todos os recursos.' },
|
||||
seguranca: { label: 'Segurança', icon: 'pi pi-shield', descricao: 'Senha, dispositivos confiáveis e sessões.' },
|
||||
// Onda 1 — pages embedadas via MelissaEmbed (1-coluna, hero glass)
|
||||
'financeiro': { label: 'Financeiro', icon: 'pi pi-wallet', descricao: 'Visão geral, recebíveis e indicadores.' },
|
||||
'financeiro-lancamentos': { label: 'Lançamentos financeiros', icon: 'pi pi-list', descricao: 'Cobranças, pagamentos e recebimentos.' },
|
||||
@@ -201,13 +203,13 @@ const MELISSA_NON_CONFIG_SLUGS = new Set([
|
||||
'tags', 'grupos', 'cadastros-recebidos', 'medicos', 'agendamentos-recebidos',
|
||||
'link-externo', 'notificacoes', 'financeiro', 'financeiro-lancamentos',
|
||||
'documentos', 'documentos-templates', 'relatorios',
|
||||
'perfil', 'plano', 'negocio', 'alterar-plano',
|
||||
'perfil', 'plano', 'negocio', 'seguranca', 'alterar-plano',
|
||||
...MELISSA_EMBED_KEYS
|
||||
]);
|
||||
// Aliases "bonitos" + INLINE_KEYS reconhecidos pelo MelissaConfiguracoes.
|
||||
// Mantidos sincronizados com o ROUTE_ALIASES/INLINE_KEYS de lá.
|
||||
const MELISSA_CONFIG_ALIASES = new Set([
|
||||
'aparencia', 'seguranca', 'bloqueios',
|
||||
'aparencia', 'bloqueios',
|
||||
'fundo', 'relogio', 'cronometro'
|
||||
]);
|
||||
function isMelissaConfigRoute(slug) {
|
||||
@@ -2237,6 +2239,11 @@ function onKeydown(e) {
|
||||
@goto="abrirSecao"
|
||||
/>
|
||||
|
||||
<MelissaSeguranca
|
||||
v-if="layoutReady && secaoAberta === 'seguranca'"
|
||||
@close="fecharSecao"
|
||||
/>
|
||||
|
||||
<MelissaConfiguracoes
|
||||
v-if="layoutReady && isMelissaConfigRoute(secaoAberta)"
|
||||
:secao-rota="secaoAberta"
|
||||
|
||||
@@ -0,0 +1,963 @@
|
||||
<script setup>
|
||||
/*
|
||||
* MelissaSeguranca — Pagina nativa Melissa pra "Seguranca".
|
||||
*
|
||||
* Substitui o embed cfg-seguranca que vivia dentro do MelissaConfiguracoes.
|
||||
* Layout 2-col:
|
||||
* - COL 1 (sidebar) — Card "Estado da conta" (email + sessao ativa) +
|
||||
* Card "Boas praticas" (4 dicas)
|
||||
* - COL 2 (main) — Card "Trocar senha" (3 password inputs + barra de
|
||||
* forca + botoes Enviar link / Trocar senha)
|
||||
*
|
||||
* Logica espelhada do SecurityPage.vue (auth.signInWithPassword pra
|
||||
* confirmar senha atual + auth.updateUser pra trocar + signOut global
|
||||
* apos sucesso por seguranca; reset por email opcional).
|
||||
*/
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
// InputText/Password/Button: auto via PrimeVueResolver
|
||||
|
||||
const emit = defineEmits(['close']);
|
||||
|
||||
const toast = useToast();
|
||||
|
||||
// ── Breakpoints + drawer ───────────────────────────────────
|
||||
const drawerOpen = ref(false);
|
||||
const isMobile = ref(false);
|
||||
let _mqMobile = null;
|
||||
function _onMqMobileChange(e) {
|
||||
isMobile.value = e.matches;
|
||||
if (!e.matches) drawerOpen.value = false;
|
||||
}
|
||||
function toggleDrawer() { drawerOpen.value = !drawerOpen.value; }
|
||||
function fecharDrawer() { drawerOpen.value = false; }
|
||||
|
||||
// ── Estado ─────────────────────────────────────────────────
|
||||
const userEmail = ref('');
|
||||
const loading = ref(false);
|
||||
const loadingReset = ref(false);
|
||||
const done = ref(false);
|
||||
|
||||
const currentPassword = ref('');
|
||||
const newPassword = ref('');
|
||||
const confirmPassword = ref('');
|
||||
|
||||
// ── Validações de senha ────────────────────────────────────
|
||||
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 strengthLabel = computed(() => {
|
||||
const labels = ['', 'Muito fraca', 'Fraca', 'Boa', 'Forte'];
|
||||
return labels[strengthScore.value] || '';
|
||||
});
|
||||
|
||||
const strengthColors = ['', '#ef4444', '#eab308', '#3b82f6', '#10b981'];
|
||||
const strengthColor = computed(() => strengthColors[strengthScore.value] || 'transparent');
|
||||
|
||||
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
|
||||
);
|
||||
|
||||
// ── Logout forte (apos trocar senha) ───────────────────────
|
||||
async function hardLogout() {
|
||||
try { await supabase.auth.signOut({ scope: 'global' }); }
|
||||
catch { /* ignore */ }
|
||||
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 { /* ignore */ }
|
||||
try { sessionStorage.removeItem('redirect_after_login'); }
|
||||
catch { /* ignore */ }
|
||||
window.location.replace('/auth/login');
|
||||
}
|
||||
|
||||
function clearFields() {
|
||||
currentPassword.value = '';
|
||||
newPassword.value = '';
|
||||
confirmPassword.value = '';
|
||||
}
|
||||
|
||||
// ── Trocar senha ───────────────────────────────────────────
|
||||
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.');
|
||||
|
||||
const { error: signError } = await supabase.auth.signInWithPassword({
|
||||
email,
|
||||
password: currentPassword.value
|
||||
});
|
||||
if (signError) throw new Error('Senha atual incorreta.');
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Reset por e-mail ───────────────────────────────────────
|
||||
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 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;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Lifecycle ──────────────────────────────────────────────
|
||||
onMounted(async () => {
|
||||
if (typeof window !== 'undefined' && window.matchMedia) {
|
||||
_mqMobile = window.matchMedia('(max-width: 1023px)');
|
||||
isMobile.value = _mqMobile.matches;
|
||||
try { _mqMobile.addEventListener('change', _onMqMobileChange); }
|
||||
catch { _mqMobile.addListener(_onMqMobileChange); }
|
||||
}
|
||||
try {
|
||||
const { data } = await supabase.auth.getUser();
|
||||
userEmail.value = data?.user?.email || '';
|
||||
} catch { /* ignore */ }
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Transition name="mse-drawer-fade">
|
||||
<div
|
||||
v-show="isMobile && drawerOpen"
|
||||
class="mse-mobile-drawer"
|
||||
:class="{ 'is-open': drawerOpen }"
|
||||
>
|
||||
<div id="mse-mobile-drawer-target" class="mse-mobile-drawer__scroll" />
|
||||
</div>
|
||||
</Transition>
|
||||
<Transition name="mse-drawer-fade">
|
||||
<div
|
||||
v-show="isMobile && drawerOpen"
|
||||
class="mse-mobile-drawer__backdrop"
|
||||
@click="fecharDrawer"
|
||||
/>
|
||||
</Transition>
|
||||
|
||||
<section class="mse-page">
|
||||
<header class="mse-page__head">
|
||||
<button
|
||||
class="mse-menu-btn mse-menu-btn--mobile-only"
|
||||
v-tooltip.bottom="'Estado & Boas práticas'"
|
||||
@click="toggleDrawer"
|
||||
>
|
||||
<i class="pi pi-bars" />
|
||||
<span>Menu</span>
|
||||
</button>
|
||||
<div class="mse-page__title">
|
||||
<i class="pi pi-shield mse-page__title-icon" />
|
||||
<span>Segurança</span>
|
||||
</div>
|
||||
<div class="mse-page__actions">
|
||||
<button class="mse-close" v-tooltip.bottom="'Voltar (Esc)'" @click="emit('close')">
|
||||
<i class="pi pi-times" />
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="mse-subheader">
|
||||
<i class="pi pi-info-circle mse-subheader__icon" />
|
||||
<span class="mse-subheader__text">
|
||||
Gerencie a senha e o acesso da sua conta. Ao trocar a senha,
|
||||
você será deslogado de todos os dispositivos por segurança.
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="mse-body">
|
||||
<Teleport to="#mse-mobile-drawer-target" :disabled="!isMobile">
|
||||
<aside class="mse-side">
|
||||
<div class="mse-side__scroll">
|
||||
<!-- Card: Estado da conta -->
|
||||
<div class="mse-w mse-w--side">
|
||||
<div class="mse-w__head">
|
||||
<div class="mse-w__icon"><i class="pi pi-id-card" /></div>
|
||||
<div class="mse-w__title">
|
||||
<div class="mse-w__title-text">Estado da conta</div>
|
||||
<div class="mse-w__sub">Sessão e identificação</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mse-w__body">
|
||||
<div class="mse-info">
|
||||
<div class="mse-info__row">
|
||||
<span class="mse-info__label">E-mail</span>
|
||||
<span class="mse-info__value mse-info__value--mono">{{ userEmail || '—' }}</span>
|
||||
</div>
|
||||
<div class="mse-info__row">
|
||||
<span class="mse-info__label">Sessão</span>
|
||||
<span class="mse-tag-active">
|
||||
<span class="mse-tag-active__dot" />
|
||||
Ativa
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mse-account-warn">
|
||||
<i class="pi pi-exclamation-triangle" />
|
||||
<span>Trocar a senha desconecta todos os dispositivos.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Card: Boas práticas -->
|
||||
<div class="mse-w mse-w--side">
|
||||
<div class="mse-w__head">
|
||||
<div class="mse-w__icon"><i class="pi pi-lightbulb" /></div>
|
||||
<div class="mse-w__title">
|
||||
<div class="mse-w__title-text">Boas práticas</div>
|
||||
<div class="mse-w__sub">Como criar uma senha segura</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mse-w__body">
|
||||
<ul class="mse-tips">
|
||||
<li class="mse-tip">
|
||||
<span class="mse-tip__bullet" style="background: #6366f1" />
|
||||
<span>Use 8+ caracteres com maiúscula, minúscula e número.</span>
|
||||
</li>
|
||||
<li class="mse-tip">
|
||||
<span class="mse-tip__bullet" style="background: #10b981" />
|
||||
<span>Evite datas, nomes e sequências óbvias (1234, qwerty).</span>
|
||||
</li>
|
||||
<li class="mse-tip">
|
||||
<span class="mse-tip__bullet" style="background: #d946ef" />
|
||||
<span>Em computador compartilhado, encerre a sessão depois.</span>
|
||||
</li>
|
||||
<li class="mse-tip">
|
||||
<span class="mse-tip__bullet" style="background: #f59e0b" />
|
||||
<span>Não reutilize senhas de outros serviços.</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</Teleport>
|
||||
|
||||
<div class="mse-main">
|
||||
<!-- Card: Trocar senha -->
|
||||
<div class="mse-w">
|
||||
<div class="mse-w__head">
|
||||
<div class="mse-w__icon"><i class="pi pi-key" /></div>
|
||||
<div class="mse-w__title">
|
||||
<div class="mse-w__title-text">Trocar senha</div>
|
||||
<div class="mse-w__sub">Confirme a senha atual e defina uma nova</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mse-w__body">
|
||||
<!-- Estado: concluído -->
|
||||
<div v-if="done" class="mse-done">
|
||||
<div class="mse-done__icon">
|
||||
<i class="pi pi-check" />
|
||||
</div>
|
||||
<div class="mse-done__title">Senha atualizada!</div>
|
||||
<div class="mse-done__hint">Redirecionando para o login…</div>
|
||||
</div>
|
||||
|
||||
<!-- Estado: form -->
|
||||
<template v-else>
|
||||
<!-- Senha atual -->
|
||||
<div class="mse-field">
|
||||
<label for="mse_current" class="mse-field__label">Senha atual</label>
|
||||
<Password
|
||||
id="mse_current"
|
||||
v-model="currentPassword"
|
||||
placeholder="Digite sua senha atual"
|
||||
toggleMask
|
||||
:feedback="false"
|
||||
class="w-full"
|
||||
inputClass="w-full"
|
||||
:disabled="loading || loadingReset"
|
||||
/>
|
||||
<small class="mse-hint">Necessária para confirmar que é você.</small>
|
||||
</div>
|
||||
|
||||
<!-- Nova senha -->
|
||||
<div class="mse-field">
|
||||
<label for="mse_new" class="mse-field__label">Nova senha</label>
|
||||
<Password
|
||||
id="mse_new"
|
||||
v-model="newPassword"
|
||||
placeholder="Mínimo 8 caracteres"
|
||||
toggleMask
|
||||
:feedback="false"
|
||||
class="w-full"
|
||||
inputClass="w-full"
|
||||
:disabled="loading || loadingReset"
|
||||
/>
|
||||
|
||||
<div v-if="newPassword" class="mse-strength">
|
||||
<div class="mse-strength__bars">
|
||||
<div
|
||||
v-for="i in 4"
|
||||
:key="i"
|
||||
class="mse-strength__bar"
|
||||
:style="{
|
||||
background: i <= strengthScore ? strengthColor : 'var(--m-border)'
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
<span class="mse-strength__label" :style="{ color: strengthColor }">
|
||||
{{ strengthLabel }}
|
||||
</span>
|
||||
</div>
|
||||
<small v-else class="mse-hint">
|
||||
Critérios: 8+ caracteres, maiúscula, minúscula e número.
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<!-- Confirmar senha -->
|
||||
<div class="mse-field">
|
||||
<label for="mse_confirm" class="mse-field__label">Confirmar nova senha</label>
|
||||
<Password
|
||||
id="mse_confirm"
|
||||
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="mse-match"
|
||||
:class="matchOk ? 'is-ok' : 'is-warn'"
|
||||
>
|
||||
<i :class="matchOk ? 'pi pi-check' : 'pi pi-times'" />
|
||||
{{ matchOk ? 'As senhas conferem.' : 'As senhas não conferem.' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Aviso -->
|
||||
<div class="mse-warn">
|
||||
<i class="pi pi-info-circle" />
|
||||
<span>
|
||||
Ao trocar sua senha, você será desconectado de todos os
|
||||
dispositivos por segurança.
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Ações -->
|
||||
<div class="mse-actions">
|
||||
<button
|
||||
class="mse-btn"
|
||||
:disabled="loading || loadingReset"
|
||||
@click="sendResetEmail"
|
||||
>
|
||||
<i :class="loadingReset ? 'pi pi-spin pi-spinner' : 'pi pi-envelope'" />
|
||||
<span>Enviar link por e-mail</span>
|
||||
</button>
|
||||
<button
|
||||
class="mse-btn mse-btn--primary"
|
||||
:disabled="!canSubmit"
|
||||
@click="changePassword"
|
||||
>
|
||||
<i :class="loading ? 'pi pi-spin pi-spinner' : 'pi pi-check'" />
|
||||
<span>Trocar senha</span>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* ═══════ Page chrome ═══════ */
|
||||
.mse-page {
|
||||
position: absolute;
|
||||
inset: 6px 6px calc(var(--m-dock-h, 76px) + 6px) 6px;
|
||||
z-index: 40;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--m-bg-medium);
|
||||
backdrop-filter: blur(32px) saturate(160%);
|
||||
-webkit-backdrop-filter: blur(32px) saturate(160%);
|
||||
border: 1px solid var(--m-border);
|
||||
border-radius: 18px;
|
||||
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.4);
|
||||
overflow: hidden;
|
||||
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
|
||||
color: var(--m-text);
|
||||
animation: mse-page-enter 240ms cubic-bezier(0.2, 0.7, 0.3, 1);
|
||||
}
|
||||
@keyframes mse-page-enter {
|
||||
from { opacity: 0; transform: scale(0.985); }
|
||||
to { opacity: 1; transform: scale(1); }
|
||||
}
|
||||
|
||||
.mse-page__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 14px 18px;
|
||||
border-bottom: 1px solid var(--m-border);
|
||||
flex-shrink: 0;
|
||||
gap: 10px;
|
||||
}
|
||||
.mse-page__title {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
color: var(--m-text);
|
||||
}
|
||||
.mse-page__title-icon { color: var(--p-primary-color); font-size: 1.05rem; }
|
||||
.mse-page__actions { display: flex; align-items: center; gap: 6px; }
|
||||
|
||||
.mse-close {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
background: transparent;
|
||||
border: 1px solid var(--m-border);
|
||||
color: var(--m-text-muted);
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: background-color 120ms ease, color 120ms ease;
|
||||
}
|
||||
.mse-close:hover { background: var(--m-bg-soft-hover); color: var(--m-text); }
|
||||
.mse-menu-btn {
|
||||
display: none;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 7px 12px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--m-border);
|
||||
background: var(--m-bg-soft);
|
||||
color: var(--m-text);
|
||||
cursor: pointer;
|
||||
font-size: 0.82rem;
|
||||
font-weight: 600;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.mse-menu-btn:hover { background: var(--m-bg-soft-hover); }
|
||||
|
||||
.mse-subheader {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
padding: 10px 18px;
|
||||
background: var(--m-bg-soft);
|
||||
border-bottom: 1px solid var(--m-border);
|
||||
color: var(--m-text-muted);
|
||||
font-size: 0.78rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.mse-subheader__icon {
|
||||
color: var(--p-primary-color);
|
||||
font-size: 0.85rem;
|
||||
margin-top: 2px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* ═══════ Body 2-col ═══════ */
|
||||
.mse-body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
}
|
||||
.mse-side {
|
||||
width: 320px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--m-bg-soft);
|
||||
border-right: 1px solid var(--m-border);
|
||||
overflow: hidden;
|
||||
}
|
||||
.mse-side__scroll {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--m-border-strong) transparent;
|
||||
}
|
||||
.mse-side__scroll::-webkit-scrollbar { width: 5px; }
|
||||
.mse-side__scroll::-webkit-scrollbar-thumb {
|
||||
background: var(--m-border-strong);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.mse-main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--m-border-strong) transparent;
|
||||
}
|
||||
.mse-main::-webkit-scrollbar { width: 5px; }
|
||||
.mse-main::-webkit-scrollbar-thumb {
|
||||
background: var(--m-border-strong);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
/* ═══════ Card-base (estilo MelissaPerfil) ═══════ */
|
||||
.mse-w {
|
||||
background: var(--m-bg-soft);
|
||||
border: 1px solid var(--m-border);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.mse-w--side {
|
||||
background: var(--m-bg-medium);
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.16);
|
||||
}
|
||||
.mse-w__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 12px 14px;
|
||||
border-bottom: 1px solid var(--m-border);
|
||||
}
|
||||
.mse-w__icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border-radius: 9px;
|
||||
flex-shrink: 0;
|
||||
background: color-mix(in srgb, var(--p-primary-color) 15%, transparent);
|
||||
color: var(--p-primary-color);
|
||||
}
|
||||
.mse-w__icon > i { font-size: 0.95rem; }
|
||||
.mse-w__title { flex: 1; min-width: 0; }
|
||||
.mse-w__title-text {
|
||||
font-size: 0.92rem;
|
||||
font-weight: 700;
|
||||
color: var(--m-text);
|
||||
line-height: 1.2;
|
||||
}
|
||||
.mse-w__sub {
|
||||
font-size: 0.74rem;
|
||||
color: var(--m-text-muted);
|
||||
margin-top: 2px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
.mse-w__body {
|
||||
padding: 14px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
/* ═══════ Sidebar: Estado da conta ═══════ */
|
||||
.mse-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
.mse-info__row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
.mse-info__label {
|
||||
font-size: 0.7rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
color: var(--m-text-muted);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.mse-info__value {
|
||||
font-size: 0.78rem;
|
||||
font-weight: 600;
|
||||
color: var(--m-text);
|
||||
text-align: right;
|
||||
word-break: break-word;
|
||||
}
|
||||
.mse-info__value--mono {
|
||||
font-family: 'JetBrains Mono', ui-monospace, monospace;
|
||||
font-size: 0.74rem;
|
||||
user-select: all;
|
||||
}
|
||||
.mse-tag-active {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 3px 10px;
|
||||
border-radius: 999px;
|
||||
background: rgba(16, 185, 129, 0.12);
|
||||
border: 1px solid rgba(16, 185, 129, 0.30);
|
||||
color: rgb(16, 185, 129);
|
||||
font-size: 0.72rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
.mse-tag-active__dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: currentColor;
|
||||
animation: mse-pulse 1.6s ease-in-out infinite;
|
||||
}
|
||||
@keyframes mse-pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.45; }
|
||||
}
|
||||
.mse-account-warn {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 9px;
|
||||
background: rgba(217, 119, 6, 0.10);
|
||||
border: 1px solid rgba(217, 119, 6, 0.30);
|
||||
color: rgb(217, 119, 6);
|
||||
font-size: 0.74rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.mse-account-warn > i { font-size: 0.85rem; margin-top: 2px; flex-shrink: 0; }
|
||||
|
||||
/* ═══════ Sidebar: Boas praticas ═══════ */
|
||||
.mse-tips {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
.mse-tip {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
font-size: 0.78rem;
|
||||
color: var(--m-text-muted);
|
||||
line-height: 1.4;
|
||||
}
|
||||
.mse-tip__bullet {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
margin-top: 6px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* ═══════ Main: Trocar senha ═══════ */
|
||||
.mse-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
.mse-field__label {
|
||||
font-size: 0.82rem;
|
||||
font-weight: 700;
|
||||
color: var(--m-text);
|
||||
}
|
||||
.mse-hint, .mse-err {
|
||||
display: block;
|
||||
font-size: 0.72rem;
|
||||
line-height: 1.3;
|
||||
color: var(--m-text-muted);
|
||||
}
|
||||
|
||||
.mse-strength {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
.mse-strength__bars {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
.mse-strength__bar {
|
||||
flex: 1;
|
||||
height: 4px;
|
||||
border-radius: 999px;
|
||||
transition: background-color 200ms ease;
|
||||
}
|
||||
.mse-strength__label {
|
||||
font-size: 0.72rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.mse-match {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
margin-top: 2px;
|
||||
}
|
||||
.mse-match.is-ok { color: rgb(16, 185, 129); }
|
||||
.mse-match.is-warn { color: rgb(217, 119, 6); }
|
||||
|
||||
.mse-warn {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 9px;
|
||||
background: var(--m-bg-medium);
|
||||
border: 1px solid var(--m-border);
|
||||
color: var(--m-text-muted);
|
||||
font-size: 0.78rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.mse-warn > i { font-size: 0.92rem; margin-top: 2px; flex-shrink: 0; color: var(--p-primary-color); }
|
||||
|
||||
.mse-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.mse-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
padding: 8px 14px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--m-border);
|
||||
background: var(--m-bg-medium);
|
||||
color: var(--m-text);
|
||||
cursor: pointer;
|
||||
font-size: 0.82rem;
|
||||
font-weight: 600;
|
||||
font-family: inherit;
|
||||
transition: background-color 120ms ease, opacity 120ms ease;
|
||||
}
|
||||
.mse-btn:hover { background: var(--m-bg-soft-hover); }
|
||||
.mse-btn:disabled { opacity: 0.45; cursor: not-allowed; }
|
||||
.mse-btn--primary {
|
||||
background: var(--p-primary-color);
|
||||
border-color: var(--p-primary-color);
|
||||
color: var(--p-primary-contrast-color, #fff);
|
||||
margin-left: auto;
|
||||
}
|
||||
.mse-btn--primary:hover {
|
||||
background: color-mix(in srgb, var(--p-primary-color) 88%, black);
|
||||
}
|
||||
|
||||
.mse-done {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
gap: 8px;
|
||||
padding: 32px 12px;
|
||||
}
|
||||
.mse-done__icon {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border-radius: 50%;
|
||||
background: rgba(16, 185, 129, 0.12);
|
||||
border: 1px solid rgba(16, 185, 129, 0.30);
|
||||
color: rgb(16, 185, 129);
|
||||
font-size: 2rem;
|
||||
}
|
||||
.mse-done__title {
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
color: var(--m-text);
|
||||
}
|
||||
.mse-done__hint {
|
||||
font-size: 0.82rem;
|
||||
color: var(--m-text-muted);
|
||||
}
|
||||
|
||||
/* ═══════ Mobile drawer ═══════ */
|
||||
.mse-mobile-drawer {
|
||||
position: fixed;
|
||||
top: 0; left: 0;
|
||||
height: 100vh;
|
||||
height: 100dvh;
|
||||
width: min(360px, 88vw);
|
||||
z-index: 80;
|
||||
background: var(--m-bg-medium);
|
||||
backdrop-filter: blur(28px) saturate(160%);
|
||||
-webkit-backdrop-filter: blur(28px) saturate(160%);
|
||||
border-right: 1px solid var(--m-border);
|
||||
transform: translateX(-100%);
|
||||
transition: transform 250ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
color: var(--m-text);
|
||||
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.mse-mobile-drawer.is-open { transform: translateX(0); }
|
||||
.mse-mobile-drawer__scroll {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.mse-mobile-drawer__scroll .mse-side {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
background: transparent;
|
||||
border-right: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.mse-mobile-drawer__scroll .mse-side__scroll {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--m-border-strong) transparent;
|
||||
}
|
||||
.mse-mobile-drawer__scroll .mse-side__scroll::-webkit-scrollbar { width: 5px; }
|
||||
.mse-mobile-drawer__scroll .mse-side__scroll::-webkit-scrollbar-thumb {
|
||||
background: var(--m-border-strong);
|
||||
border-radius: 3px;
|
||||
}
|
||||
.mse-mobile-drawer__scroll .mse-w--side {
|
||||
margin: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.mse-mobile-drawer__backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.45);
|
||||
backdrop-filter: blur(4px);
|
||||
-webkit-backdrop-filter: blur(4px);
|
||||
z-index: 79;
|
||||
}
|
||||
.mse-drawer-fade-enter-active,
|
||||
.mse-drawer-fade-leave-active { transition: opacity 200ms ease; }
|
||||
.mse-drawer-fade-enter-from,
|
||||
.mse-drawer-fade-leave-to { opacity: 0; }
|
||||
|
||||
/* ═══════ Desktop (>=1024px): cards min-h 300 + body scroll ═══════ */
|
||||
@media (min-width: 1024px) {
|
||||
.mse-side > .mse-side__scroll > .mse-w--side {
|
||||
min-height: 300px;
|
||||
max-height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.mse-side .mse-w--side > .mse-w__body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--m-border-strong) transparent;
|
||||
}
|
||||
.mse-side .mse-w--side > .mse-w__body::-webkit-scrollbar { width: 5px; }
|
||||
.mse-side .mse-w--side > .mse-w__body::-webkit-scrollbar-thumb {
|
||||
background: var(--m-border-strong);
|
||||
border-radius: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ═══════ Mobile (<1024px) ═══════ */
|
||||
@media (max-width: 1023px) {
|
||||
.mse-body { flex-direction: column; padding: 0; }
|
||||
.mse-side { display: none; }
|
||||
.mse-main { width: 100%; padding: 8px; }
|
||||
.mse-main .mse-w {
|
||||
height: auto;
|
||||
flex: 0 0 auto;
|
||||
align-self: stretch;
|
||||
}
|
||||
.mse-page__title > span:first-of-type { display: none; }
|
||||
.mse-page__title-icon { display: none; }
|
||||
.mse-menu-btn--mobile-only { display: inline-flex; }
|
||||
.mse-actions { flex-direction: column; }
|
||||
.mse-btn--primary { margin-left: 0; }
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user