443 lines
13 KiB
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>
|