first commit
This commit is contained in:
@@ -0,0 +1,374 @@
|
||||
<template>
|
||||
<div class="min-h-[calc(100vh-8rem)] p-4 md:p-6">
|
||||
<div class="mx-auto w-full max-w-4xl">
|
||||
<Card class="overflow-hidden rounded-2xl border border-[var(--surface-border)] bg-[var(--surface-card)] shadow-sm">
|
||||
<template #title>
|
||||
<div class="relative">
|
||||
<!-- blobs sutis -->
|
||||
<div class="pointer-events-none absolute inset-0 opacity-90">
|
||||
<div class="absolute -top-20 -right-16 h-60 w-60 rounded-full bg-indigo-400/10 blur-3xl" />
|
||||
<div class="absolute top-6 -left-20 h-72 w-72 rounded-full bg-emerald-400/10 blur-3xl" />
|
||||
<div class="absolute -bottom-24 right-10 h-64 w-64 rounded-full bg-fuchsia-400/10 blur-3xl" />
|
||||
</div>
|
||||
|
||||
<div class="relative flex flex-col gap-2 p-5 md:p-6">
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div class="min-w-0">
|
||||
<div class="flex items-center gap-3">
|
||||
<div
|
||||
class="grid h-10 w-10 place-items-center rounded-xl border border-[var(--surface-border)] bg-[var(--surface-ground)]"
|
||||
>
|
||||
<i class="pi pi-shield text-lg opacity-80" />
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<div class="text-xl md:text-2xl font-semibold leading-tight">
|
||||
Segurança
|
||||
</div>
|
||||
<div class="mt-0.5 text-sm md:text-base text-[var(--text-color-secondary)]">
|
||||
Troque sua senha com cuidado. Depois, você será deslogado por segurança.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hidden md:flex items-center gap-2">
|
||||
<span
|
||||
class="inline-flex items-center gap-2 rounded-full border border-[var(--surface-border)] bg-[var(--surface-ground)] px-3 py-1 text-xs text-[var(--text-color-secondary)]"
|
||||
>
|
||||
<span class="h-1.5 w-1.5 rounded-full bg-emerald-400/70" />
|
||||
sessão ativa
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #content>
|
||||
<div class="p-5 md:p-6 pt-0">
|
||||
<!-- GRID -->
|
||||
<div class="grid grid-cols-12 gap-4">
|
||||
<!-- Senha atual -->
|
||||
<div class="col-span-12 md:col-span-6">
|
||||
<div class="rounded-2xl border border-[var(--surface-border)] bg-[var(--surface-ground)] p-4">
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<div class="text-sm font-semibold">Senha atual</div>
|
||||
<div class="mt-1 text-xs text-[var(--text-color-secondary)]">
|
||||
Necessária para confirmar que é você.
|
||||
</div>
|
||||
</div>
|
||||
<i class="pi pi-lock text-sm opacity-70" />
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<Password
|
||||
v-model="currentPassword"
|
||||
toggleMask
|
||||
:feedback="false"
|
||||
inputClass="w-full"
|
||||
:disabled="loading || loadingReset"
|
||||
placeholder="Digite sua senha atual"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Dica lateral -->
|
||||
<div class="col-span-12 md:col-span-6">
|
||||
<div class="h-full rounded-2xl border border-[var(--surface-border)] bg-[var(--surface-ground)] p-4">
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<div class="text-sm font-semibold">Boas práticas</div>
|
||||
<div class="mt-1 text-xs text-[var(--text-color-secondary)]">
|
||||
Senhas fortes são menos “lembráveis”, mas mais seguras.
|
||||
</div>
|
||||
</div>
|
||||
<i class="pi pi-info-circle text-sm opacity-70" />
|
||||
</div>
|
||||
|
||||
<ul class="mt-3 space-y-2 text-xs text-[var(--text-color-secondary)]">
|
||||
<li class="flex gap-2">
|
||||
<span class="mt-1 h-1.5 w-1.5 rounded-full bg-indigo-400/70"></span>
|
||||
Use pelo menos 8 caracteres, com maiúscula, minúscula e número.
|
||||
</li>
|
||||
<li class="flex gap-2">
|
||||
<span class="mt-1 h-1.5 w-1.5 rounded-full bg-emerald-400/70"></span>
|
||||
Evite datas, nomes e padrões (1234, qwerty).
|
||||
</li>
|
||||
<li class="flex gap-2">
|
||||
<span class="mt-1 h-1.5 w-1.5 rounded-full bg-fuchsia-400/70"></span>
|
||||
Se estiver em computador público, finalize a sessão depois.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Nova senha -->
|
||||
<div class="col-span-12 md:col-span-6">
|
||||
<div class="rounded-2xl border border-[var(--surface-border)] bg-[var(--surface-ground)] p-4">
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<div class="text-sm font-semibold">Nova senha</div>
|
||||
<div class="mt-1 text-xs text-[var(--text-color-secondary)]">
|
||||
Deve atender aos critérios mínimos.
|
||||
</div>
|
||||
</div>
|
||||
<i class="pi pi-key text-sm opacity-70" />
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<Password
|
||||
v-model="newPassword"
|
||||
toggleMask
|
||||
:feedback="false"
|
||||
inputClass="w-full"
|
||||
:disabled="loading || loadingReset"
|
||||
placeholder="Crie uma nova senha"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-2 text-xs">
|
||||
<span
|
||||
v-if="!newPassword"
|
||||
class="text-[var(--text-color-secondary)]"
|
||||
>
|
||||
Critérios: 8+ caracteres, maiúscula, minúscula e número.
|
||||
</span>
|
||||
<span
|
||||
v-else
|
||||
:class="passwordStrengthOk ? 'text-emerald-500' : 'text-yellow-500'"
|
||||
>
|
||||
{{ passwordStrengthOk ? 'Senha forte o suficiente.' : 'Ainda está fraca — ajuste os critérios.' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Confirmar -->
|
||||
<div class="col-span-12 md:col-span-6">
|
||||
<div class="rounded-2xl border border-[var(--surface-border)] bg-[var(--surface-ground)] p-4">
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<div class="text-sm font-semibold">Confirmar nova senha</div>
|
||||
<div class="mt-1 text-xs text-[var(--text-color-secondary)]">
|
||||
Evita erro de digitação.
|
||||
</div>
|
||||
</div>
|
||||
<i class="pi pi-check-circle text-sm opacity-70" />
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<Password
|
||||
v-model="confirmPassword"
|
||||
toggleMask
|
||||
:feedback="false"
|
||||
inputClass="w-full"
|
||||
:disabled="loading || loadingReset"
|
||||
placeholder="Repita a nova senha"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-2 text-xs">
|
||||
<span v-if="!confirmPassword" class="text-[var(--text-color-secondary)]">
|
||||
Digite novamente para confirmar.
|
||||
</span>
|
||||
<span
|
||||
v-else
|
||||
:class="passwordMatchOk ? 'text-emerald-500' : 'text-yellow-500'"
|
||||
>
|
||||
{{ passwordMatchOk ? 'Confere.' : 'Não confere com a nova senha.' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Ações -->
|
||||
<div class="mt-5 flex flex-col-reverse gap-2 md:flex-row md:items-center md:justify-between">
|
||||
<div class="text-xs text-[var(--text-color-secondary)]">
|
||||
Ao trocar sua senha, você será desconectado de forma global.
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2 sm:flex-row sm:justify-end">
|
||||
<Button
|
||||
label="Esqueci minha senha"
|
||||
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="loading || loadingReset"
|
||||
@click="changePassword"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import Card from 'primevue/card'
|
||||
import Password from 'primevue/password'
|
||||
import Button from 'primevue/button'
|
||||
import { useToast } from 'primevue/usetoast'
|
||||
import { supabase } from '@/lib/supabase/client'
|
||||
import { sessionUser, sessionRole } from '@/app/session'
|
||||
|
||||
const toast = useToast()
|
||||
|
||||
const currentPassword = ref('')
|
||||
const newPassword = ref('')
|
||||
const confirmPassword = ref('')
|
||||
const loading = ref(false)
|
||||
const loadingReset = ref(false)
|
||||
|
||||
function isStrongEnough (p) {
|
||||
return /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$/.test(p || '')
|
||||
}
|
||||
|
||||
const passwordStrengthOk = computed(() => isStrongEnough(newPassword.value))
|
||||
const passwordMatchOk = computed(() => !!confirmPassword.value && newPassword.value === confirmPassword.value)
|
||||
|
||||
function clearFields () {
|
||||
currentPassword.value = ''
|
||||
newPassword.value = ''
|
||||
confirmPassword.value = ''
|
||||
}
|
||||
|
||||
async function hardLogout () {
|
||||
// 1) tenta logout normal (se falhar, seguimos)
|
||||
try {
|
||||
// DEBUG LOGOUT
|
||||
console.log('ANTES', (await supabase.auth.getSession()).data.session)
|
||||
await supabase.auth.signOut({ scope: 'global' })
|
||||
console.log('DEPOIS', (await supabase.auth.getSession()).data.session)
|
||||
} catch (e) {
|
||||
console.warn('[signOut failed]', e)
|
||||
}
|
||||
|
||||
// 2) zera estado reativo global
|
||||
sessionUser.value = null
|
||||
sessionRole.value = null
|
||||
|
||||
// 3) remove token persistido do supabase-js v2 (sb-*-auth-token)
|
||||
try {
|
||||
const keysToRemove = []
|
||||
for (let i = 0; i < localStorage.length; i++) {
|
||||
const k = localStorage.key(i)
|
||||
if (!k) continue
|
||||
if (k.startsWith('sb-') && k.includes('auth-token')) keysToRemove.push(k)
|
||||
}
|
||||
keysToRemove.forEach((k) => localStorage.removeItem(k))
|
||||
} catch (e) {
|
||||
console.warn('[storage cleanup failed]', e)
|
||||
}
|
||||
|
||||
// 4) remove redirect pendente
|
||||
try {
|
||||
sessionStorage.removeItem('redirect_after_login')
|
||||
} catch {}
|
||||
|
||||
// 5) redireciona de forma "hard"
|
||||
window.location.replace('/auth/login')
|
||||
}
|
||||
|
||||
async function changePassword () {
|
||||
const user = sessionUser.value
|
||||
|
||||
if (!user?.email) {
|
||||
toast.add({ severity: 'error', summary: 'Sessão', detail: 'Usuário não encontrado na sessão.', life: 3500 })
|
||||
return
|
||||
}
|
||||
|
||||
if (!currentPassword.value || !newPassword.value || !confirmPassword.value) {
|
||||
toast.add({ severity: 'warn', summary: 'Campos', detail: 'Preencha todos os campos.', life: 3000 })
|
||||
return
|
||||
}
|
||||
|
||||
if (newPassword.value !== confirmPassword.value) {
|
||||
toast.add({ severity: 'warn', summary: 'Confirmação', detail: 'A confirmação não confere.', life: 3000 })
|
||||
return
|
||||
}
|
||||
|
||||
if (!isStrongEnough(newPassword.value)) {
|
||||
toast.add({
|
||||
severity: 'warn',
|
||||
summary: 'Senha fraca',
|
||||
detail: 'Use no mínimo 8 caracteres com maiúscula, minúscula e número.',
|
||||
life: 4500
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
// Reautentica (padrão mais previsível)
|
||||
const { error: signError } = await supabase.auth.signInWithPassword({
|
||||
email: user.email,
|
||||
password: currentPassword.value
|
||||
})
|
||||
if (signError) throw signError
|
||||
|
||||
const { error: upError } = await supabase.auth.updateUser({
|
||||
password: newPassword.value
|
||||
})
|
||||
if (upError) throw upError
|
||||
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
summary: 'Senha atualizada',
|
||||
detail: 'Por segurança, você será deslogado.',
|
||||
life: 2500
|
||||
})
|
||||
|
||||
clearFields()
|
||||
await hardLogout()
|
||||
} 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
|
||||
}
|
||||
}
|
||||
|
||||
async function sendResetEmail () {
|
||||
const user = sessionUser.value
|
||||
if (!user?.email) {
|
||||
toast.add({ severity: 'error', summary: 'Sessão', detail: 'Usuário não encontrado na sessão.', life: 3500 })
|
||||
return
|
||||
}
|
||||
|
||||
loadingReset.value = true
|
||||
try {
|
||||
const redirectTo = `${window.location.origin}/auth/reset-password`
|
||||
const { error } = await supabase.auth.resetPasswordForEmail(user.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
|
||||
}
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user