Files
sgse-app/apps/web/src/routes/(dashboard)/alterar-senha/+page.svelte

443 lines
13 KiB
Svelte

<script lang="ts">
import { api } from '@sgse-app/backend/convex/_generated/api';
import { useConvexClient, useQuery } from 'convex-svelte';
import {
AlertCircle,
CheckCircle2,
Eye,
EyeOff,
Info,
Key,
Lock,
Shield,
XCircle
} from 'lucide-svelte';
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
import { resolve } from '$app/paths';
const convex = useConvexClient();
const currentUser = useQuery(api.auth.getCurrentUser, {});
let senhaAtual = $state('');
let novaSenha = $state('');
let confirmarSenha = $state('');
let carregando = $state(false);
let notice = $state<{ type: 'success' | 'error'; message: string } | null>(null);
let mostrarSenhaAtual = $state(false);
let mostrarNovaSenha = $state(false);
let mostrarConfirmarSenha = $state(false);
onMount(() => {
if (!currentUser?.data) {
goto(resolve('/'));
}
});
function validarSenha(senha: string): { valido: boolean; erros: string[] } {
const erros: string[] = [];
if (senha.length < 8) {
erros.push('A senha deve ter no mínimo 8 caracteres');
}
if (!/[A-Z]/.test(senha)) {
erros.push('A senha deve conter pelo menos uma letra maiúscula');
}
if (!/[a-z]/.test(senha)) {
erros.push('A senha deve conter pelo menos uma letra minúscula');
}
if (!/[0-9]/.test(senha)) {
erros.push('A senha deve conter pelo menos um número');
}
if (!/[!@#$%^&*(),.?":{}|<>]/.test(senha)) {
erros.push('A senha deve conter pelo menos um caractere especial');
}
return {
valido: erros.length === 0,
erros
};
}
async function handleSubmit(e: Event) {
e.preventDefault();
notice = null;
// Validações
if (!senhaAtual || !novaSenha || !confirmarSenha) {
notice = {
type: 'error',
message: 'Todos os campos são obrigatórios'
};
return;
}
if (novaSenha !== confirmarSenha) {
notice = {
type: 'error',
message: 'A nova senha e a confirmação não coincidem'
};
return;
}
if (senhaAtual === novaSenha) {
notice = {
type: 'error',
message: 'A nova senha deve ser diferente da senha atual'
};
return;
}
const validacao = validarSenha(novaSenha);
if (!validacao.valido) {
notice = {
type: 'error',
message: validacao.erros.join('. ')
};
return;
}
carregando = true;
try {
// Construir objeto de argumentos, incluindo token apenas se existir
const args: {
senhaAtual: string;
novaSenha: string;
token?: string;
} = {
senhaAtual: senhaAtual,
novaSenha: novaSenha
};
// Adicionar token apenas se existir
if (authStore.token) {
args.token = authStore.token;
}
const resultado = await convex.mutation(api.autenticacao.alterarSenha, args);
if (resultado.sucesso) {
notice = {
type: 'success',
message: 'Senha alterada com sucesso! Redirecionando...'
};
// Limpar campos
senhaAtual = '';
novaSenha = '';
confirmarSenha = '';
// Redirecionar após 2 segundos
setTimeout(() => {
goto(resolve('/'));
}, 2000);
} else {
notice = {
type: 'error',
message: resultado.erro || 'Erro ao alterar senha'
};
}
} catch (error: any) {
notice = {
type: 'error',
message: error.message || 'Erro ao conectar com o servidor'
};
} finally {
carregando = false;
}
}
function cancelar() {
goto(resolve('/'));
}
</script>
<main class="container mx-auto max-w-4xl px-4 py-8">
<!-- Header Moderno -->
<div class="mb-8">
<div
class="from-primary/20 via-primary/10 to-primary/20 border-primary/20 mb-6 rounded-2xl border bg-linear-to-r p-6 shadow-lg"
>
<div class="flex items-center gap-4">
<div class="bg-primary/20 rounded-2xl p-3">
<Key class="text-primary h-8 w-8" strokeWidth={2.5} />
</div>
<div class="flex-1">
<h1 class="text-primary mb-2 text-4xl font-bold">Alterar Senha</h1>
<p class="text-base-content/70 text-lg">
Atualize sua senha de acesso ao sistema de forma segura
</p>
</div>
<div class="badge badge-primary badge-lg gap-2">
<Shield class="h-4 w-4" strokeWidth={2} />
Seguro
</div>
</div>
</div>
<!-- Breadcrumbs -->
<div class="breadcrumbs mb-6 text-sm">
<ul>
<li><a href={resolve('/')} class="link link-hover">Dashboard</a></li>
<li>Alterar Senha</li>
</ul>
</div>
</div>
<!-- Alertas -->
{#if notice}
<div
class="alert {notice.type === 'success'
? 'alert-success'
: 'alert-error'} animate-in fade-in slide-in-from-top mb-6 shadow-xl duration-300"
>
{#if notice.type === 'success'}
<CheckCircle2 class="h-6 w-6 shrink-0 stroke-current" strokeWidth={2} />
{:else}
<XCircle class="h-6 w-6 shrink-0 stroke-current" strokeWidth={2} />
{/if}
<span class="font-semibold">{notice.message}</span>
</div>
{/if}
<div class="grid grid-cols-1 gap-6 lg:grid-cols-3">
<!-- Formulário Principal -->
<div class="lg:col-span-2">
<div
class="card bg-base-100 border-base-300/50 border-2 shadow-xl transition-all duration-300 hover:shadow-2xl"
>
<div class="card-body p-8">
<div class="mb-6 flex items-center gap-3">
<div class="bg-primary/10 rounded-xl p-2">
<Lock class="text-primary h-6 w-6" strokeWidth={2} />
</div>
<h2 class="text-base-content text-2xl font-bold">Formulário de Alteração</h2>
</div>
<form onsubmit={handleSubmit} class="space-y-6">
<!-- Senha Atual -->
<div class="form-control">
<label class="label" for="senha-atual">
<span class="label-text text-base font-semibold">Senha Atual</span>
<span class="label-text-alt text-error font-bold">*</span>
</label>
<div class="relative">
<input
id="senha-atual"
type={mostrarSenhaAtual ? 'text' : 'password'}
placeholder="Digite sua senha atual"
class="input input-bordered input-primary h-12 w-full pr-12 text-base"
bind:value={senhaAtual}
required
disabled={carregando}
/>
<button
type="button"
class="btn btn-sm btn-ghost btn-circle hover:bg-primary/10 absolute top-1/2 right-2 -translate-y-1/2"
onclick={() => (mostrarSenhaAtual = !mostrarSenhaAtual)}
disabled={carregando}
aria-label={mostrarSenhaAtual ? 'Ocultar senha' : 'Mostrar senha'}
>
{#if mostrarSenhaAtual}
<EyeOff class="text-base-content/60 h-5 w-5" strokeWidth={2} />
{:else}
<Eye class="text-base-content/60 h-5 w-5" strokeWidth={2} />
{/if}
</button>
</div>
</div>
<!-- Nova Senha -->
<div class="form-control">
<label class="label" for="nova-senha">
<span class="label-text text-base font-semibold">Nova Senha</span>
<span class="label-text-alt text-error font-bold">*</span>
</label>
<div class="relative">
<input
id="nova-senha"
type={mostrarNovaSenha ? 'text' : 'password'}
placeholder="Digite sua nova senha"
class="input input-bordered input-primary h-12 w-full pr-12 text-base"
bind:value={novaSenha}
required
disabled={carregando}
/>
<button
type="button"
class="btn btn-sm btn-ghost btn-circle hover:bg-primary/10 absolute top-1/2 right-2 -translate-y-1/2"
onclick={() => (mostrarNovaSenha = !mostrarNovaSenha)}
disabled={carregando}
aria-label={mostrarNovaSenha ? 'Ocultar senha' : 'Mostrar senha'}
>
{#if mostrarNovaSenha}
<EyeOff class="text-base-content/60 h-5 w-5" strokeWidth={2} />
{:else}
<Eye class="text-base-content/60 h-5 w-5" strokeWidth={2} />
{/if}
</button>
</div>
<div class="label">
<span class="label-text-alt text-base-content/60 text-xs">
Mínimo 8 caracteres com maiúsculas, minúsculas, números e especiais
</span>
</div>
</div>
<!-- Confirmar Senha -->
<div class="form-control">
<label class="label" for="confirmar-senha">
<span class="label-text text-base font-semibold">Confirmar Nova Senha</span>
<span class="label-text-alt text-error font-bold">*</span>
</label>
<div class="relative">
<input
id="confirmar-senha"
type={mostrarConfirmarSenha ? 'text' : 'password'}
placeholder="Digite novamente sua nova senha"
class="input input-bordered input-primary h-12 w-full pr-12 text-base"
bind:value={confirmarSenha}
required
disabled={carregando}
/>
<button
type="button"
class="btn btn-sm btn-ghost btn-circle hover:bg-primary/10 absolute top-1/2 right-2 -translate-y-1/2"
onclick={() => (mostrarConfirmarSenha = !mostrarConfirmarSenha)}
disabled={carregando}
aria-label={mostrarConfirmarSenha ? 'Ocultar senha' : 'Mostrar senha'}
>
{#if mostrarConfirmarSenha}
<EyeOff class="text-base-content/60 h-5 w-5" strokeWidth={2} />
{:else}
<Eye class="text-base-content/60 h-5 w-5" strokeWidth={2} />
{/if}
</button>
</div>
</div>
<!-- Botões -->
<div
class="border-base-300 mt-8 flex flex-col justify-end gap-4 border-t pt-6 sm:flex-row"
>
<button
type="button"
class="btn btn-outline btn-lg flex-1 sm:flex-initial"
onclick={cancelar}
disabled={carregando}
>
<XCircle class="h-5 w-5" strokeWidth={2} />
Cancelar
</button>
<button
type="submit"
class="btn btn-primary btn-lg flex-1 shadow-lg transition-all duration-200 hover:shadow-xl sm:flex-initial"
disabled={carregando}
>
{#if carregando}
<span class="loading loading-spinner loading-sm"></span>
Alterando...
{:else}
<CheckCircle2 class="h-5 w-5" strokeWidth={2} />
Alterar Senha
{/if}
</button>
</div>
</form>
</div>
</div>
</div>
<!-- Sidebar com Informações -->
<div class="space-y-6">
<!-- Requisitos de Senha -->
<div class="card from-info/10 to-info/5 border-info/20 border-2 bg-linear-to-br shadow-lg">
<div class="card-body p-6">
<div class="mb-4 flex items-center gap-3">
<div class="bg-info/20 rounded-xl p-2">
<Info class="text-info h-6 w-6" strokeWidth={2} />
</div>
<h3 class="text-base-content text-lg font-bold">Requisitos de Senha</h3>
</div>
<ul class="space-y-3 text-sm">
<li class="flex items-start gap-2">
<CheckCircle2 class="text-success mt-0.5 h-5 w-5 shrink-0" strokeWidth={2} />
<span class="text-base-content/80">Mínimo de 8 caracteres</span>
</li>
<li class="flex items-start gap-2">
<CheckCircle2 class="text-success mt-0.5 h-5 w-5 shrink-0" strokeWidth={2} />
<span class="text-base-content/80">Pelo menos uma letra maiúscula (A-Z)</span>
</li>
<li class="flex items-start gap-2">
<CheckCircle2 class="text-success mt-0.5 h-5 w-5 shrink-0" strokeWidth={2} />
<span class="text-base-content/80">Pelo menos uma letra minúscula (a-z)</span>
</li>
<li class="flex items-start gap-2">
<CheckCircle2 class="text-success mt-0.5 h-5 w-5 shrink-0" strokeWidth={2} />
<span class="text-base-content/80">Pelo menos um número (0-9)</span>
</li>
<li class="flex items-start gap-2">
<CheckCircle2 class="text-success mt-0.5 h-5 w-5 shrink-0" strokeWidth={2} />
<span class="text-base-content/80">Pelo menos um caractere especial (!@#$%...)</span>
</li>
</ul>
</div>
</div>
<!-- Dicas de Segurança -->
<div
class="card from-warning/10 to-warning/5 border-warning/20 border-2 bg-linear-to-br shadow-lg"
>
<div class="card-body p-6">
<div class="mb-4 flex items-center gap-3">
<div class="bg-warning/20 rounded-xl p-2">
<Shield class="text-warning h-6 w-6" strokeWidth={2} />
</div>
<h3 class="text-base-content text-lg font-bold">Dicas de Segurança</h3>
</div>
<ul class="space-y-3 text-sm">
<li class="flex items-start gap-2">
<AlertCircle class="text-warning mt-0.5 h-5 w-5 shrink-0" strokeWidth={2} />
<span class="text-base-content/80">Nunca compartilhe sua senha</span>
</li>
<li class="flex items-start gap-2">
<AlertCircle class="text-warning mt-0.5 h-5 w-5 shrink-0" strokeWidth={2} />
<span class="text-base-content/80">Use uma senha única para cada sistema</span>
</li>
<li class="flex items-start gap-2">
<AlertCircle class="text-warning mt-0.5 h-5 w-5 shrink-0" strokeWidth={2} />
<span class="text-base-content/80">Altere sua senha regularmente</span>
</li>
<li class="flex items-start gap-2">
<AlertCircle class="text-warning mt-0.5 h-5 w-5 shrink-0" strokeWidth={2} />
<span class="text-base-content/80">Evite informações pessoais óbvias</span>
</li>
<li class="flex items-start gap-2">
<AlertCircle class="text-warning mt-0.5 h-5 w-5 shrink-0" strokeWidth={2} />
<span class="text-base-content/80">Considere usar um gerenciador de senhas</span>
</li>
</ul>
</div>
</div>
</div>
</div>
</main>
<style>
@keyframes fade-in {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.animate-in {
animation: fade-in 0.3s ease-out;
}
</style>