From 2ca9cde2ea21b50bb31d887f2a6cc8c9d5a23bf9 Mon Sep 17 00:00:00 2001 From: Leonardo Date: Wed, 6 May 2026 15:57:27 -0300 Subject: [PATCH] MelissaSeguranca: pagina nativa 2-col + grupo "Conta" sai inteiro de Configuracoes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 `` - '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) --- src/layout/melissa/MelissaConfiguracoes.vue | 23 +- src/layout/melissa/MelissaLayout.vue | 13 +- src/layout/melissa/MelissaSeguranca.vue | 963 ++++++++++++++++++++ 3 files changed, 979 insertions(+), 20 deletions(-) create mode 100644 src/layout/melissa/MelissaSeguranca.vue diff --git a/src/layout/melissa/MelissaConfiguracoes.vue b/src/layout/melissa/MelissaConfiguracoes.vue index 79ca87b..76ac319 100644 --- a/src/layout/melissa/MelissaConfiguracoes.vue +++ b/src/layout/melissa/MelissaConfiguracoes.vue @@ -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/. { key: 'agenda', label: 'Agenda', diff --git a/src/layout/melissa/MelissaLayout.vue b/src/layout/melissa/MelissaLayout.vue index 2325b8d..a534f43 100644 --- a/src/layout/melissa/MelissaLayout.vue +++ b/src/layout/melissa/MelissaLayout.vue @@ -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 — 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 */ } +}); + + + + +