feat: implement advanced access control system with user blocking, rate limiting, and enhanced login security; update UI components for improved user experience and documentation

This commit is contained in:
2025-10-29 09:07:37 -03:00
parent d1715f358a
commit 6b14059fde
33 changed files with 6450 additions and 1202 deletions

View File

@@ -1,6 +1,8 @@
import { v } from "convex/values";
import { mutation, query } from "./_generated/server";
import { hashPassword } from "./auth/utils";
import { hashPassword, generateToken } from "./auth/utils";
import { registrarAtividade } from "./logsAtividades";
import { Id } from "./_generated/dataModel";
/**
* Criar novo usuário (apenas TI)
@@ -76,6 +78,8 @@ export const listar = query({
nome: v.string(),
email: v.string(),
ativo: v.boolean(),
bloqueado: v.optional(v.boolean()),
motivoBloqueio: v.optional(v.string()),
primeiroAcesso: v.boolean(),
ultimoAcesso: v.optional(v.number()),
criadoEm: v.number(),
@@ -141,6 +145,8 @@ export const listar = query({
nome: usuario.nome,
email: usuario.email,
ativo: usuario.ativo,
bloqueado: usuario.bloqueado,
motivoBloqueio: usuario.motivoBloqueio,
primeiroAcesso: usuario.primeiroAcesso,
ultimoAcesso: usuario.ultimoAcesso,
criadoEm: usuario.criadoEm,
@@ -569,3 +575,362 @@ export const uploadFotoPerfil = mutation({
},
});
// ==================== GESTÃO AVANÇADA DE USUÁRIOS (TI_MASTER) ====================
/**
* Bloquear usuário (apenas TI_MASTER)
*/
export const bloquearUsuario = mutation({
args: {
usuarioId: v.id("usuarios"),
motivo: v.string(),
bloqueadoPorId: v.id("usuarios"),
},
returns: v.union(
v.object({ sucesso: v.literal(true) }),
v.object({ sucesso: v.literal(false), erro: v.string() })
),
handler: async (ctx, args) => {
const usuario = await ctx.db.get(args.usuarioId);
if (!usuario) {
return { sucesso: false as const, erro: "Usuário não encontrado" };
}
// Atualizar usuário como bloqueado
await ctx.db.patch(args.usuarioId, {
bloqueado: true,
motivoBloqueio: args.motivo,
dataBloqueio: Date.now(),
atualizadoEm: Date.now(),
});
// Registrar no histórico de bloqueios
await ctx.db.insert("bloqueiosUsuarios", {
usuarioId: args.usuarioId,
motivo: args.motivo,
bloqueadoPor: args.bloqueadoPorId,
dataInicio: Date.now(),
ativo: true,
});
// Desativar todas as sessões ativas do usuário
const sessoes = await ctx.db
.query("sessoes")
.withIndex("by_usuario", (q) => q.eq("usuarioId", args.usuarioId))
.filter((q) => q.eq(q.field("ativo"), true))
.collect();
for (const sessao of sessoes) {
await ctx.db.patch(sessao._id, { ativo: false });
}
// Log de atividade
await registrarAtividade(
ctx,
args.bloqueadoPorId,
"bloquear",
"usuarios",
JSON.stringify({ usuarioId: args.usuarioId, motivo: args.motivo }),
args.usuarioId
);
return { sucesso: true as const };
},
});
/**
* Desbloquear usuário (apenas TI_MASTER)
*/
export const desbloquearUsuario = mutation({
args: {
usuarioId: v.id("usuarios"),
desbloqueadoPorId: v.id("usuarios"),
},
returns: v.union(
v.object({ sucesso: v.literal(true) }),
v.object({ sucesso: v.literal(false), erro: v.string() })
),
handler: async (ctx, args) => {
const usuario = await ctx.db.get(args.usuarioId);
if (!usuario) {
return { sucesso: false as const, erro: "Usuário não encontrado" };
}
// Atualizar usuário como desbloqueado
await ctx.db.patch(args.usuarioId, {
bloqueado: false,
motivoBloqueio: undefined,
dataBloqueio: undefined,
tentativasLogin: 0,
ultimaTentativaLogin: undefined,
atualizadoEm: Date.now(),
});
// Fechar bloqueios ativos
const bloqueiosAtivos = await ctx.db
.query("bloqueiosUsuarios")
.withIndex("by_usuario", (q) => q.eq("usuarioId", args.usuarioId))
.filter((q) => q.eq(q.field("ativo"), true))
.collect();
for (const bloqueio of bloqueiosAtivos) {
await ctx.db.patch(bloqueio._id, {
ativo: false,
dataFim: Date.now(),
desbloqueadoPor: args.desbloqueadoPorId,
});
}
// Log de atividade
await registrarAtividade(
ctx,
args.desbloqueadoPorId,
"desbloquear",
"usuarios",
JSON.stringify({ usuarioId: args.usuarioId }),
args.usuarioId
);
return { sucesso: true as const };
},
});
/**
* Resetar senha de usuário (apenas TI_MASTER)
*/
export const resetarSenhaUsuario = mutation({
args: {
usuarioId: v.id("usuarios"),
resetadoPorId: v.id("usuarios"),
novaSenhaTemporaria: v.optional(v.string()), // Se não fornecer, gera automática
},
returns: v.union(
v.object({ sucesso: v.literal(true), senhaTemporaria: v.string() }),
v.object({ sucesso: v.literal(false), erro: v.string() })
),
handler: async (ctx, args) => {
const usuario = await ctx.db.get(args.usuarioId);
if (!usuario) {
return { sucesso: false as const, erro: "Usuário não encontrado" };
}
// Gerar senha temporária se não foi fornecida
const senhaTemporaria = args.novaSenhaTemporaria || gerarSenhaTemporaria();
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(),
});
// Log de atividade
await registrarAtividade(
ctx,
args.resetadoPorId,
"resetar_senha",
"usuarios",
JSON.stringify({ usuarioId: args.usuarioId }),
args.usuarioId
);
return { sucesso: true as const, senhaTemporaria };
},
});
// Helper para gerar senha temporária
function gerarSenhaTemporaria(): string {
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789@#$%";
let senha = "";
for (let i = 0; i < 12; i++) {
senha += chars.charAt(Math.floor(Math.random() * chars.length));
}
return senha;
}
/**
* Editar dados de usuário (apenas TI_MASTER)
*/
export const editarUsuario = mutation({
args: {
usuarioId: v.id("usuarios"),
nome: v.optional(v.string()),
email: v.optional(v.string()),
roleId: v.optional(v.id("roles")),
setor: v.optional(v.string()),
editadoPorId: v.id("usuarios"),
},
returns: v.union(
v.object({ sucesso: v.literal(true) }),
v.object({ sucesso: v.literal(false), erro: v.string() })
),
handler: async (ctx, args) => {
const usuario = await ctx.db.get(args.usuarioId);
if (!usuario) {
return { sucesso: false as const, erro: "Usuário não encontrado" };
}
// Verificar se email já existe (se estiver mudando)
if (args.email && args.email !== usuario.email) {
const emailExistente = await ctx.db
.query("usuarios")
.withIndex("by_email", (q) => q.eq("email", args.email!))
.first();
if (emailExistente) {
return { sucesso: false as const, erro: "E-mail já cadastrado" };
}
}
// Atualizar campos fornecidos
const updates: any = {
atualizadoEm: Date.now(),
};
if (args.nome !== undefined) updates.nome = args.nome;
if (args.email !== undefined) updates.email = args.email;
if (args.roleId !== undefined) updates.roleId = args.roleId;
if (args.setor !== undefined) updates.setor = args.setor;
await ctx.db.patch(args.usuarioId, updates);
// Log de atividade
await registrarAtividade(
ctx,
args.editadoPorId,
"editar",
"usuarios",
JSON.stringify(updates),
args.usuarioId
);
return { sucesso: true as const };
},
});
/**
* Desativar usuário logicamente (soft delete - apenas TI_MASTER)
*/
export const excluirUsuarioLogico = mutation({
args: {
usuarioId: v.id("usuarios"),
excluidoPorId: v.id("usuarios"),
},
returns: v.union(
v.object({ sucesso: v.literal(true) }),
v.object({ sucesso: v.literal(false), erro: v.string() })
),
handler: async (ctx, args) => {
const usuario = await ctx.db.get(args.usuarioId);
if (!usuario) {
return { sucesso: false as const, erro: "Usuário não encontrado" };
}
// Marcar como inativo
await ctx.db.patch(args.usuarioId, {
ativo: false,
atualizadoEm: Date.now(),
});
// Desativar todas as sessões
const sessoes = await ctx.db
.query("sessoes")
.withIndex("by_usuario", (q) => q.eq("usuarioId", args.usuarioId))
.filter((q) => q.eq(q.field("ativo"), true))
.collect();
for (const sessao of sessoes) {
await ctx.db.patch(sessao._id, { ativo: false });
}
// Log de atividade
await registrarAtividade(
ctx,
args.excluidoPorId,
"excluir",
"usuarios",
JSON.stringify({ usuarioId: args.usuarioId }),
args.usuarioId
);
return { sucesso: true as const };
},
});
/**
* Criar usuário completo com permissões (TI_MASTER)
*/
export const criarUsuarioCompleto = mutation({
args: {
matricula: v.string(),
nome: v.string(),
email: v.string(),
roleId: v.id("roles"),
setor: v.optional(v.string()),
senhaInicial: v.optional(v.string()),
criadoPorId: v.id("usuarios"),
enviarEmailBoasVindas: v.optional(v.boolean()),
},
returns: v.union(
v.object({ sucesso: v.literal(true), usuarioId: v.id("usuarios"), senhaTemporaria: v.string() }),
v.object({ sucesso: v.literal(false), erro: v.string() })
),
handler: async (ctx, args) => {
// Verificar se matrícula já existe
const existente = await ctx.db
.query("usuarios")
.withIndex("by_matricula", (q) => q.eq("matricula", args.matricula))
.first();
if (existente) {
return { sucesso: false as const, erro: "Matrícula já cadastrada" };
}
// Verificar se email já existe
const emailExistente = await ctx.db
.query("usuarios")
.withIndex("by_email", (q) => q.eq("email", args.email))
.first();
if (emailExistente) {
return { sucesso: false as const, erro: "E-mail já cadastrado" };
}
// Gerar senha inicial se não fornecida
const senhaTemporaria = args.senhaInicial || gerarSenhaTemporaria();
const senhaHash = await hashPassword(senhaTemporaria);
// Criar usuário
const usuarioId = await ctx.db.insert("usuarios", {
matricula: args.matricula,
senhaHash,
nome: args.nome,
email: args.email,
roleId: args.roleId,
setor: args.setor,
ativo: true,
primeiroAcesso: true,
criadoEm: Date.now(),
atualizadoEm: Date.now(),
});
// Log de atividade
await registrarAtividade(
ctx,
args.criadoPorId,
"criar",
"usuarios",
JSON.stringify({ usuarioId, matricula: args.matricula, nome: args.nome }),
usuarioId
);
// TODO: Se enviarEmailBoasVindas = true, enfileirar email
// Isso será implementado quando criarmos o sistema de emails
return { sucesso: true as const, usuarioId, senhaTemporaria };
},
});