feat: add password reset functionality for users, including modal interface for generating temporary passwords and copying to clipboard; enhance backend mutation for secure password management and email notifications
This commit is contained in:
@@ -18,7 +18,9 @@
|
|||||||
Check,
|
Check,
|
||||||
UserPlus,
|
UserPlus,
|
||||||
Search,
|
Search,
|
||||||
MoreVertical
|
MoreVertical,
|
||||||
|
Copy,
|
||||||
|
CheckCircle2
|
||||||
} from 'lucide-svelte';
|
} from 'lucide-svelte';
|
||||||
|
|
||||||
type AvisoUsuario = {
|
type AvisoUsuario = {
|
||||||
@@ -189,6 +191,9 @@
|
|||||||
let buscaFuncionario = $state('');
|
let buscaFuncionario = $state('');
|
||||||
|
|
||||||
let modalExcluirAberto = $state(false);
|
let modalExcluirAberto = $state(false);
|
||||||
|
let modalResetarSenhaAberto = $state(false);
|
||||||
|
let novaSenhaGerada = $state<string | null>(null);
|
||||||
|
let senhaCopiada = $state(false);
|
||||||
let processando = $state(false);
|
let processando = $state(false);
|
||||||
let mensagem = $state<Mensagem | null>(null);
|
let mensagem = $state<Mensagem | null>(null);
|
||||||
|
|
||||||
@@ -525,6 +530,68 @@
|
|||||||
usuarioSelecionado = null;
|
usuarioSelecionado = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function abrirModalResetarSenha(usuario: Usuario) {
|
||||||
|
usuarioSelecionado = usuario;
|
||||||
|
novaSenhaGerada = null;
|
||||||
|
modalResetarSenhaAberto = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function fecharModalResetarSenha() {
|
||||||
|
modalResetarSenhaAberto = false;
|
||||||
|
novaSenhaGerada = null;
|
||||||
|
senhaCopiada = false;
|
||||||
|
usuarioSelecionado = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function copiarSenha() {
|
||||||
|
if (!novaSenhaGerada) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(novaSenhaGerada);
|
||||||
|
senhaCopiada = true;
|
||||||
|
setTimeout(() => {
|
||||||
|
senhaCopiada = false;
|
||||||
|
}, 2000);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro ao copiar senha:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resetarSenha() {
|
||||||
|
if (!usuarioSelecionado || !currentUser?.data) {
|
||||||
|
console.error('Erro: usuarioSelecionado ou currentUser não definido');
|
||||||
|
mostrarMensagem('error', 'Erro: dados do usuário não encontrados');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
processando = true;
|
||||||
|
console.log('Iniciando reset de senha para:', usuarioSelecionado._id);
|
||||||
|
|
||||||
|
const resultado = await client.mutation(api.usuarios.resetarSenhaUsuario, {
|
||||||
|
usuarioId: usuarioSelecionado._id,
|
||||||
|
resetadoPorId: currentUser.data._id
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Resultado do reset:', resultado);
|
||||||
|
|
||||||
|
if (resultado && resultado.sucesso) {
|
||||||
|
novaSenhaGerada = resultado.senhaTemporaria;
|
||||||
|
mostrarMensagem('success', 'Senha resetada com sucesso! Email enviado ao usuário.');
|
||||||
|
} else {
|
||||||
|
const erroMsg = resultado?.erro || 'Erro ao resetar senha';
|
||||||
|
console.error('Erro no reset:', erroMsg);
|
||||||
|
mostrarMensagem('error', erroMsg);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro ao resetar senha:', error);
|
||||||
|
const errorMessage = error instanceof Error ? error.message : 'Erro desconhecido ao resetar senha';
|
||||||
|
mostrarMensagem('error', errorMessage);
|
||||||
|
} finally {
|
||||||
|
processando = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function excluirUsuario() {
|
async function excluirUsuario() {
|
||||||
if (!usuarioSelecionado) {
|
if (!usuarioSelecionado) {
|
||||||
return;
|
return;
|
||||||
@@ -912,6 +979,17 @@
|
|||||||
{usuario.funcionario ? 'Alterar Funcionário' : 'Associar Funcionário'}
|
{usuario.funcionario ? 'Alterar Funcionário' : 'Associar Funcionário'}
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onclick={() => abrirModalResetarSenha(usuario)}
|
||||||
|
disabled={processando}
|
||||||
|
class="text-warning"
|
||||||
|
>
|
||||||
|
Resetar Senha
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<div class="divider my-0"></div>
|
||||||
<li>
|
<li>
|
||||||
{#if usuario.bloqueado}
|
{#if usuario.bloqueado}
|
||||||
<button
|
<button
|
||||||
@@ -1133,4 +1211,103 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
<!-- Modal Resetar Senha -->
|
||||||
|
{#if modalResetarSenhaAberto && usuarioSelecionado}
|
||||||
|
<div class="modal modal-open">
|
||||||
|
<div class="modal-box">
|
||||||
|
<h3 class="mb-4 text-lg font-bold">Resetar Senha do Usuário</h3>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<p class="text-base-content/80 mb-2">
|
||||||
|
<strong>Usuário:</strong>
|
||||||
|
{usuarioSelecionado.nome} ({usuarioSelecionado.matricula})
|
||||||
|
</p>
|
||||||
|
<p class="text-base-content/70 mb-4 text-sm">
|
||||||
|
Uma nova senha temporária será gerada e enviada por email ao usuário.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{#if novaSenhaGerada}
|
||||||
|
<div class="alert alert-success mb-4">
|
||||||
|
<CheckCircle class="h-6 w-6 shrink-0 stroke-current" strokeWidth={2} />
|
||||||
|
<div class="flex-1 w-full">
|
||||||
|
<p class="font-semibold text-base">Senha resetada com sucesso!</p>
|
||||||
|
<p class="mt-3 text-sm font-medium">
|
||||||
|
<strong>Nova senha temporária:</strong>
|
||||||
|
</p>
|
||||||
|
<div class="bg-base-200 border-base-300 mt-2 flex items-center justify-between gap-2 rounded-lg border p-4">
|
||||||
|
<code class="text-xl font-mono font-bold select-all">{novaSenhaGerada}</code>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm btn-ghost shrink-0"
|
||||||
|
onclick={copiarSenha}
|
||||||
|
aria-label="Copiar senha"
|
||||||
|
title="Copiar senha"
|
||||||
|
>
|
||||||
|
{#if senhaCopiada}
|
||||||
|
<CheckCircle2 class="h-5 w-5 text-success" strokeWidth={2} />
|
||||||
|
{:else}
|
||||||
|
<Copy class="h-5 w-5" strokeWidth={2} />
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p class="mt-3 text-xs text-base-content/70">
|
||||||
|
✓ Esta senha foi enviada por email para <strong>{usuarioSelecionado.email}</strong>
|
||||||
|
</p>
|
||||||
|
<p class="mt-1 text-xs text-base-content/60">
|
||||||
|
O usuário precisará alterar a senha no próximo login.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="alert alert-warning">
|
||||||
|
<AlertTriangle class="h-6 w-6 shrink-0 stroke-current" strokeWidth={2} />
|
||||||
|
<span>
|
||||||
|
O usuário precisará alterar a senha no próximo login. Todas as sessões ativas serão encerradas.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-action">
|
||||||
|
{#if novaSenhaGerada}
|
||||||
|
<button type="button" class="btn btn-primary" onclick={fecharModalResetarSenha}>
|
||||||
|
Fechar
|
||||||
|
</button>
|
||||||
|
{:else}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn"
|
||||||
|
onclick={fecharModalResetarSenha}
|
||||||
|
disabled={processando}
|
||||||
|
>
|
||||||
|
Cancelar
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-warning"
|
||||||
|
onclick={resetarSenha}
|
||||||
|
disabled={processando}
|
||||||
|
>
|
||||||
|
{#if processando}
|
||||||
|
<span class="loading loading-spinner loading-sm"></span>
|
||||||
|
{/if}
|
||||||
|
Resetar Senha
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-backdrop">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onclick={fecharModalResetarSenha}
|
||||||
|
onkeydown={(e) => e.key === 'Escape' && fecharModalResetarSenha()}
|
||||||
|
aria-label="Fechar modal"
|
||||||
|
class="sr-only"
|
||||||
|
>
|
||||||
|
Fechar
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
|
|||||||
@@ -841,48 +841,107 @@ export const desbloquearUsuario = mutation({
|
|||||||
/**
|
/**
|
||||||
* Resetar senha de usuário (apenas TI_MASTER)
|
* Resetar senha de usuário (apenas TI_MASTER)
|
||||||
*/
|
*/
|
||||||
// export const resetarSenhaUsuario = mutation({
|
export const resetarSenhaUsuario = mutation({
|
||||||
// args: {
|
args: {
|
||||||
// usuarioId: v.id("usuarios"),
|
usuarioId: v.id('usuarios'),
|
||||||
// resetadoPorId: v.id("usuarios"),
|
resetadoPorId: v.id('usuarios'),
|
||||||
// novaSenhaTemporaria: v.optional(v.string()), // Se não fornecer, gera automática
|
novaSenhaTemporaria: v.optional(v.string()) // Se não fornecer, gera automática
|
||||||
// },
|
},
|
||||||
// returns: v.union(
|
returns: v.union(
|
||||||
// v.object({ sucesso: v.literal(true), senhaTemporaria: v.string() }),
|
v.object({ sucesso: v.literal(true), senhaTemporaria: v.string() }),
|
||||||
// v.object({ sucesso: v.literal(false), erro: v.string() })
|
v.object({ sucesso: v.literal(false), erro: v.string() })
|
||||||
// ),
|
),
|
||||||
// handler: async (ctx, args) => {
|
handler: async (ctx, args) => {
|
||||||
// const usuario = await ctx.db.get(args.usuarioId);
|
const usuario = await ctx.db.get(args.usuarioId);
|
||||||
// if (!usuario) {
|
if (!usuario) {
|
||||||
// return { sucesso: false as const, erro: "Usuário não encontrado" };
|
return { sucesso: false as const, erro: 'Usuário não encontrado' };
|
||||||
// }
|
}
|
||||||
|
|
||||||
// // Gerar senha temporária se não foi fornecida
|
// Verificar permissão (apenas TI_MASTER)
|
||||||
// const senhaTemporaria = args.novaSenhaTemporaria || gerarSenhaTemporaria();
|
const resetadoPor = await ctx.db.get(args.resetadoPorId);
|
||||||
// const senhaHash = await hashPassword(senhaTemporaria);
|
if (!resetadoPor) {
|
||||||
|
return { sucesso: false as const, erro: 'Usuário que está resetando não encontrado' };
|
||||||
|
}
|
||||||
|
|
||||||
// // Atualizar usuário
|
// Buscar a role do usuário
|
||||||
// await ctx.db.patch(args.usuarioId, {
|
if (!resetadoPor.roleId) {
|
||||||
// senhaHash,
|
return { sucesso: false as const, erro: 'Usuário não possui role definida' };
|
||||||
// primeiroAcesso: true, // Força mudança de senha no próximo login
|
}
|
||||||
// tentativasLogin: 0,
|
|
||||||
// ultimaTentativaLogin: undefined,
|
|
||||||
// atualizadoEm: Date.now(),
|
|
||||||
// });
|
|
||||||
|
|
||||||
// // Log de atividade
|
const role = await ctx.db.get(resetadoPor.roleId);
|
||||||
// await registrarAtividade(
|
if (!role) {
|
||||||
// ctx,
|
return { sucesso: false as const, erro: 'Role do usuário não encontrada' };
|
||||||
// args.resetadoPorId,
|
}
|
||||||
// "resetar_senha",
|
|
||||||
// "usuarios",
|
|
||||||
// JSON.stringify({ usuarioId: args.usuarioId }),
|
|
||||||
// args.usuarioId
|
|
||||||
// );
|
|
||||||
|
|
||||||
// return { sucesso: true as const, senhaTemporaria };
|
// Permitir TI_MASTER, TI_USUARIO e ADMIN
|
||||||
// },
|
const rolesPermitidas = ['ti_master', 'ti_usuario', 'admin'];
|
||||||
// });
|
if (!rolesPermitidas.includes(role.nome)) {
|
||||||
|
return {
|
||||||
|
sucesso: false as const,
|
||||||
|
erro: 'Apenas usuários de TI ou administradores podem resetar senhas'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gerar senha temporária se não foi fornecida
|
||||||
|
const senhaTemporaria = args.novaSenhaTemporaria || gerarSenhaTemporaria();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Fazer hash da senha
|
||||||
|
const { hashPassword } = await import('./auth/utils');
|
||||||
|
const senhaHash = await hashPassword(senhaTemporaria);
|
||||||
|
|
||||||
|
// Atualizar usuário
|
||||||
|
await ctx.db.patch(args.usuarioId, {
|
||||||
|
senhaHash,
|
||||||
|
primeiroAcesso: true, // Força mudança de senha no próximo login
|
||||||
|
tentativasLogin: 0,
|
||||||
|
ultimaTentativaLogin: undefined,
|
||||||
|
atualizadoEm: Date.now()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Desativar todas as sessões ativas
|
||||||
|
const sessoes = await ctx.db
|
||||||
|
.query('sessoes')
|
||||||
|
.withIndex('by_usuario', (q) => q.eq('usuarioId', args.usuarioId))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
for (const sessao of sessoes) {
|
||||||
|
await ctx.db.patch(sessao._id, { ativo: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enviar email com a nova senha usando template
|
||||||
|
try {
|
||||||
|
await ctx.scheduler.runAfter(0, api.email.enviarEmailComTemplate, {
|
||||||
|
destinatario: usuario.email,
|
||||||
|
destinatarioId: args.usuarioId,
|
||||||
|
templateCodigo: 'SENHA_RESETADA',
|
||||||
|
variaveis: {
|
||||||
|
senha: senhaTemporaria
|
||||||
|
},
|
||||||
|
enviadoPor: args.resetadoPorId
|
||||||
|
});
|
||||||
|
} catch (emailError) {
|
||||||
|
console.error('Erro ao agendar envio de email:', emailError);
|
||||||
|
// Não falhar a mutation se o email falhar, apenas logar o erro
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log de atividade
|
||||||
|
await registrarAtividade(
|
||||||
|
ctx,
|
||||||
|
args.resetadoPorId,
|
||||||
|
'resetar_senha',
|
||||||
|
'usuarios',
|
||||||
|
JSON.stringify({ usuarioId: args.usuarioId }),
|
||||||
|
args.usuarioId
|
||||||
|
);
|
||||||
|
|
||||||
|
return { sucesso: true as const, senhaTemporaria };
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
|
return { sucesso: false as const, erro: `Erro ao resetar senha: ${errorMessage}` };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Helper para gerar senha temporária
|
// Helper para gerar senha temporária
|
||||||
function gerarSenhaTemporaria(): string {
|
function gerarSenhaTemporaria(): string {
|
||||||
|
|||||||
Reference in New Issue
Block a user