first commit

This commit is contained in:
Leonardo
2026-02-18 22:36:45 -03:00
parent ec6b6ef53a
commit 676042268b
122 changed files with 26354 additions and 1615 deletions
+374
View File
@@ -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>