Files
sgse-app/packages/backend/convex/usuarios.ts

1016 lines
27 KiB
TypeScript

import { v } from "convex/values";
import { mutation, query } from "./_generated/server";
import { hashPassword, generateToken } from "./auth/utils";
import { registrarAtividade } from "./logsAtividades";
import { Id } from "./_generated/dataModel";
/**
* Associar funcionário a um usuário
*/
export const associarFuncionario = mutation({
args: {
usuarioId: v.id("usuarios"),
funcionarioId: v.id("funcionarios"),
},
returns: v.object({ sucesso: v.boolean() }),
handler: async (ctx, args) => {
// Verificar se o funcionário existe
const funcionario = await ctx.db.get(args.funcionarioId);
if (!funcionario) {
throw new Error("Funcionário não encontrado");
}
// Verificar se o funcionário já está associado a outro usuário
const usuarioExistente = await ctx.db
.query("usuarios")
.withIndex("by_funcionarioId", (q) => q.eq("funcionarioId", args.funcionarioId))
.first();
if (usuarioExistente && usuarioExistente._id !== args.usuarioId) {
throw new Error(
`Este funcionário já está associado ao usuário: ${usuarioExistente.nome} (${usuarioExistente.matricula})`
);
}
// Associar funcionário ao usuário
await ctx.db.patch(args.usuarioId, {
funcionarioId: args.funcionarioId,
});
return { sucesso: true };
},
});
/**
* Desassociar funcionário de um usuário
*/
export const desassociarFuncionario = mutation({
args: {
usuarioId: v.id("usuarios"),
},
returns: v.object({ sucesso: v.boolean() }),
handler: async (ctx, args) => {
await ctx.db.patch(args.usuarioId, {
funcionarioId: undefined,
});
return { sucesso: true };
},
});
/**
* Criar novo usuário (apenas TI)
*/
export const criar = mutation({
args: {
matricula: v.string(),
nome: v.string(),
email: v.string(),
roleId: v.id("roles"),
funcionarioId: v.optional(v.id("funcionarios")),
senhaInicial: v.string(),
},
returns: v.union(
v.object({ sucesso: v.literal(true), usuarioId: v.id("usuarios") }),
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 hash da senha inicial
const senhaHash = await hashPassword(args.senhaInicial);
// Criar usuário
const usuarioId = await ctx.db.insert("usuarios", {
matricula: args.matricula,
senhaHash,
nome: args.nome,
email: args.email,
funcionarioId: args.funcionarioId,
roleId: args.roleId,
ativo: true,
primeiroAcesso: true,
criadoEm: Date.now(),
atualizadoEm: Date.now(),
});
return { sucesso: true as const, usuarioId };
},
});
/**
* Listar todos os usuários com filtros
*/
export const listar = query({
args: {
setor: v.optional(v.string()),
matricula: v.optional(v.string()),
ativo: v.optional(v.boolean()),
},
returns: v.array(
v.object({
_id: v.id("usuarios"),
matricula: v.string(),
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(),
role: v.object({
_id: v.id("roles"),
nome: v.string(),
nivel: v.number(),
setor: v.optional(v.string()),
}),
funcionario: v.optional(
v.object({
_id: v.id("funcionarios"),
nome: v.string(),
simboloTipo: v.union(
v.literal("cargo_comissionado"),
v.literal("funcao_gratificada")
),
})
),
})
),
handler: async (ctx, args) => {
let usuarios = await ctx.db.query("usuarios").collect();
// Filtrar por matrícula
if (args.matricula) {
usuarios = usuarios.filter((u) =>
u.matricula.includes(args.matricula!)
);
}
// Filtrar por ativo
if (args.ativo !== undefined) {
usuarios = usuarios.filter((u) => u.ativo === args.ativo);
}
// Buscar roles e funcionários
const resultado = [];
for (const usuario of usuarios) {
const role = await ctx.db.get(usuario.roleId);
if (!role) continue;
// Filtrar por setor
if (args.setor && role.setor !== args.setor) {
continue;
}
let funcionario = undefined;
if (usuario.funcionarioId) {
const func = await ctx.db.get(usuario.funcionarioId);
if (func) {
funcionario = {
_id: func._id,
nome: func.nome,
simboloTipo: func.simboloTipo,
};
}
}
resultado.push({
_id: usuario._id,
matricula: usuario.matricula,
nome: usuario.nome,
email: usuario.email,
ativo: usuario.ativo,
bloqueado: usuario.bloqueado,
motivoBloqueio: usuario.motivoBloqueio,
primeiroAcesso: usuario.primeiroAcesso,
ultimoAcesso: usuario.ultimoAcesso,
criadoEm: usuario.criadoEm,
role: {
_id: role._id,
nome: role.nome,
nivel: role.nivel,
setor: role.setor,
},
funcionario,
});
}
return resultado;
},
});
/**
* Ativar/Desativar usuário
*/
export const alterarStatus = mutation({
args: {
usuarioId: v.id("usuarios"),
ativo: v.boolean(),
},
returns: v.null(),
handler: async (ctx, args) => {
await ctx.db.patch(args.usuarioId, {
ativo: args.ativo,
atualizadoEm: Date.now(),
});
// Se desativar, desativar todas as sessões
if (!args.ativo) {
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 });
}
}
return null;
},
});
/**
* Resetar senha do usuário
*/
export const resetarSenha = mutation({
args: {
usuarioId: v.id("usuarios"),
novaSenha: v.string(),
},
returns: v.null(),
handler: async (ctx, args) => {
const senhaHash = await hashPassword(args.novaSenha);
await ctx.db.patch(args.usuarioId, {
senhaHash,
primeiroAcesso: true,
atualizadoEm: Date.now(),
});
// Desativar todas as sessões
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 });
}
return null;
},
});
/**
* Excluir usuário
*/
export const excluir = mutation({
args: {
usuarioId: v.id("usuarios"),
},
returns: v.null(),
handler: async (ctx, args) => {
// Excluir sessões
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.delete(sessao._id);
}
// Excluir usuário
await ctx.db.delete(args.usuarioId);
return null;
},
});
/**
* Ativar usuário
*/
export const ativar = mutation({
args: {
id: v.id("usuarios"),
},
returns: v.null(),
handler: async (ctx, args) => {
await ctx.db.patch(args.id, {
ativo: true,
atualizadoEm: Date.now(),
});
return null;
},
});
/**
* Desativar usuário
*/
export const desativar = mutation({
args: {
id: v.id("usuarios"),
},
returns: v.null(),
handler: async (ctx, args) => {
await ctx.db.patch(args.id, {
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.id))
.collect();
for (const sessao of sessoes) {
await ctx.db.patch(sessao._id, { ativo: false });
}
return null;
},
});
/**
* Alterar role de um usuário
*/
export const alterarRole = mutation({
args: {
usuarioId: v.id("usuarios"),
novaRoleId: v.id("roles"),
},
returns: v.null(),
handler: async (ctx, args) => {
// Verificar se a role existe
const role = await ctx.db.get(args.novaRoleId);
if (!role) {
throw new Error("Role não encontrada");
}
// Atualizar usuário
await ctx.db.patch(args.usuarioId, {
roleId: args.novaRoleId,
atualizadoEm: Date.now(),
});
return null;
},
});
/**
* Atualizar perfil do usuário (foto, avatar, setor, status, preferências)
*/
export const atualizarPerfil = mutation({
args: {
avatar: v.optional(v.string()),
fotoPerfil: v.optional(v.id("_storage")),
setor: v.optional(v.string()),
statusMensagem: v.optional(v.string()),
statusPresenca: v.optional(
v.union(
v.literal("online"),
v.literal("offline"),
v.literal("ausente"),
v.literal("externo"),
v.literal("em_reuniao")
)
),
notificacoesAtivadas: v.optional(v.boolean()),
somNotificacao: v.optional(v.boolean()),
},
returns: v.null(),
handler: async (ctx, args) => {
// TENTAR BETTER AUTH PRIMEIRO
const identity = await ctx.auth.getUserIdentity();
let usuarioAtual = null;
if (identity && identity.email) {
// Buscar por email (Better Auth)
usuarioAtual = await ctx.db
.query("usuarios")
.withIndex("by_email", (q) => q.eq("email", identity.email!))
.first();
}
// SE NÃO ENCONTROU, BUSCAR POR SESSÃO ATIVA (Sistema customizado)
if (!usuarioAtual) {
const sessaoAtiva = await ctx.db
.query("sessoes")
.filter((q) => q.eq(q.field("ativo"), true))
.order("desc")
.first();
if (sessaoAtiva) {
usuarioAtual = await ctx.db.get(sessaoAtiva.usuarioId);
}
}
if (!usuarioAtual) throw new Error("Usuário não encontrado");
// Validar statusMensagem (max 100 chars)
if (args.statusMensagem && args.statusMensagem.length > 100) {
throw new Error("Mensagem de status deve ter no máximo 100 caracteres");
}
// Atualizar apenas os campos fornecidos
const updates: any = { atualizadoEm: Date.now() };
if (args.avatar !== undefined) updates.avatar = args.avatar;
if (args.fotoPerfil !== undefined) updates.fotoPerfil = args.fotoPerfil;
if (args.setor !== undefined) updates.setor = args.setor;
if (args.statusMensagem !== undefined) updates.statusMensagem = args.statusMensagem;
if (args.statusPresenca !== undefined) {
updates.statusPresenca = args.statusPresenca;
updates.ultimaAtividade = Date.now();
}
if (args.notificacoesAtivadas !== undefined) updates.notificacoesAtivadas = args.notificacoesAtivadas;
if (args.somNotificacao !== undefined) updates.somNotificacao = args.somNotificacao;
await ctx.db.patch(usuarioAtual._id, updates);
return null;
},
});
/**
* Obter perfil do usuário atual
*/
export const obterPerfil = query({
args: {},
returns: v.union(
v.object({
_id: v.id("usuarios"),
nome: v.string(),
email: v.string(),
matricula: v.string(),
funcionarioId: v.optional(v.id("funcionarios")),
avatar: v.optional(v.string()),
fotoPerfil: v.optional(v.id("_storage")),
fotoPerfilUrl: v.union(v.string(), v.null()),
setor: v.optional(v.string()),
statusMensagem: v.optional(v.string()),
statusPresenca: v.optional(v.union(
v.literal("online"),
v.literal("offline"),
v.literal("ausente"),
v.literal("externo"),
v.literal("em_reuniao")
)),
notificacoesAtivadas: v.boolean(),
somNotificacao: v.boolean(),
}),
v.null()
),
handler: async (ctx) => {
console.log("=== DEBUG obterPerfil ===");
// TENTAR BETTER AUTH PRIMEIRO
const identity = await ctx.auth.getUserIdentity();
console.log("Identity:", identity ? "encontrado" : "null");
let usuarioAtual = null;
if (identity && identity.email) {
console.log("Tentando buscar por email:", identity.email);
// Buscar por email (Better Auth)
usuarioAtual = await ctx.db
.query("usuarios")
.withIndex("by_email", (q) => q.eq("email", identity.email!))
.first();
console.log("Usuário encontrado por email:", usuarioAtual ? "SIM" : "NÃO");
}
// SE NÃO ENCONTROU, BUSCAR POR SESSÃO ATIVA (Sistema customizado)
if (!usuarioAtual) {
console.log("Buscando por sessão ativa...");
const sessaoAtiva = await ctx.db
.query("sessoes")
.filter((q) => q.eq(q.field("ativo"), true))
.order("desc")
.first();
console.log("Sessão ativa encontrada:", sessaoAtiva ? "SIM" : "NÃO");
if (sessaoAtiva) {
usuarioAtual = await ctx.db.get(sessaoAtiva.usuarioId);
console.log("Usuário da sessão encontrado:", usuarioAtual ? "SIM" : "NÃO");
}
}
if (!usuarioAtual) {
console.log("❌ Nenhum usuário encontrado");
// Listar todos os usuários para debug
const todosUsuarios = await ctx.db.query("usuarios").collect();
console.log("Total de usuários no banco:", todosUsuarios.length);
console.log("Emails cadastrados:", todosUsuarios.map(u => u.email));
return null;
}
console.log("✅ Usuário encontrado:", usuarioAtual.nome);
// Buscar fotoPerfil URL se existir
let fotoPerfilUrl = null;
if (usuarioAtual.fotoPerfil) {
fotoPerfilUrl = await ctx.storage.getUrl(usuarioAtual.fotoPerfil);
}
return {
_id: usuarioAtual._id,
nome: usuarioAtual.nome,
email: usuarioAtual.email,
matricula: usuarioAtual.matricula,
funcionarioId: usuarioAtual.funcionarioId,
avatar: usuarioAtual.avatar,
fotoPerfil: usuarioAtual.fotoPerfil,
fotoPerfilUrl,
setor: usuarioAtual.setor,
statusMensagem: usuarioAtual.statusMensagem,
statusPresenca: usuarioAtual.statusPresenca,
notificacoesAtivadas: usuarioAtual.notificacoesAtivadas ?? true,
somNotificacao: usuarioAtual.somNotificacao ?? true,
};
},
});
/**
* Listar todos usuários para o chat (com avatar, foto e status)
*/
export const listarParaChat = query({
args: {},
returns: v.array(
v.object({
_id: v.id("usuarios"),
nome: v.string(),
email: v.string(),
matricula: v.optional(v.string()),
avatar: v.optional(v.string()),
fotoPerfil: v.optional(v.id("_storage")),
fotoPerfilUrl: v.union(v.string(), v.null()),
statusPresenca: v.optional(
v.union(
v.literal("online"),
v.literal("offline"),
v.literal("ausente"),
v.literal("externo"),
v.literal("em_reuniao")
)
),
statusMensagem: v.optional(v.string()),
ultimaAtividade: v.optional(v.number()),
})
),
handler: async (ctx) => {
// Buscar todos os usuários ativos
const usuarios = await ctx.db
.query("usuarios")
.filter((q) => q.eq(q.field("ativo"), true))
.collect();
// Buscar foto de perfil URL para cada usuário
const usuariosComFoto = await Promise.all(
usuarios.map(async (usuario) => {
let fotoPerfilUrl = null;
if (usuario.fotoPerfil) {
fotoPerfilUrl = await ctx.storage.getUrl(usuario.fotoPerfil);
}
return {
_id: usuario._id,
nome: usuario.nome,
email: usuario.email,
matricula: usuario.matricula || undefined,
avatar: usuario.avatar,
fotoPerfil: usuario.fotoPerfil,
fotoPerfilUrl,
statusPresenca: usuario.statusPresenca || "offline",
statusMensagem: usuario.statusMensagem,
ultimaAtividade: usuario.ultimaAtividade,
};
})
);
return usuariosComFoto;
},
});
/**
* Gera URL para upload de foto de perfil
*/
export const uploadFotoPerfil = mutation({
args: {},
handler: async (ctx) => {
// TENTAR BETTER AUTH PRIMEIRO
const identity = await ctx.auth.getUserIdentity();
let usuarioAtual = null;
if (identity && identity.email) {
// Buscar por email (Better Auth)
usuarioAtual = await ctx.db
.query("usuarios")
.withIndex("by_email", (q) => q.eq("email", identity.email!))
.first();
}
// SE NÃO ENCONTROU, BUSCAR POR SESSÃO ATIVA (Sistema customizado)
if (!usuarioAtual) {
const sessaoAtiva = await ctx.db
.query("sessoes")
.filter((q) => q.eq(q.field("ativo"), true))
.order("desc")
.first();
if (sessaoAtiva) {
usuarioAtual = await ctx.db.get(sessaoAtiva.usuarioId);
}
}
if (!usuarioAtual) throw new Error("Usuário não autenticado");
return await ctx.storage.generateUploadUrl();
},
});
// ==================== 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 };
},
});