1229 lines
32 KiB
TypeScript
1229 lines
32 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";
|
|
import { api } from "./_generated/api";
|
|
|
|
/**
|
|
* 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: {},
|
|
returns: v.string(),
|
|
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 };
|
|
},
|
|
});
|
|
|
|
/**
|
|
* Criar/Promover usuário Admin Master (TI_MASTER - nível 0)
|
|
*/
|
|
export const criarAdminMaster = mutation({
|
|
args: {
|
|
matricula: v.string(),
|
|
nome: v.string(),
|
|
email: v.string(),
|
|
senha: v.optional(v.string()),
|
|
},
|
|
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) => {
|
|
// Garantir que a role TI_MASTER exista (nível 0)
|
|
let roleTIMaster = await ctx.db
|
|
.query("roles")
|
|
.withIndex("by_nome", (q) => q.eq("nome", "ti_master"))
|
|
.first();
|
|
|
|
if (!roleTIMaster) {
|
|
const roleId = await ctx.db.insert("roles", {
|
|
nome: "ti_master",
|
|
descricao: "TI Master",
|
|
nivel: 0,
|
|
setor: "ti",
|
|
customizado: false,
|
|
editavel: false,
|
|
});
|
|
roleTIMaster = await ctx.db.get(roleId);
|
|
}
|
|
|
|
if (!roleTIMaster) {
|
|
return {
|
|
sucesso: false as const,
|
|
erro: "Falha ao garantir role TI Master",
|
|
};
|
|
}
|
|
|
|
// Se já existir usuário por matrícula, promove/atualiza
|
|
const existentePorMatricula = await ctx.db
|
|
.query("usuarios")
|
|
.withIndex("by_matricula", (q) => q.eq("matricula", args.matricula))
|
|
.first();
|
|
|
|
const senhaTemporaria = args.senha || gerarSenhaTemporaria();
|
|
const senhaHash = await hashPassword(senhaTemporaria);
|
|
|
|
if (existentePorMatricula) {
|
|
await ctx.db.patch(existentePorMatricula._id, {
|
|
nome: args.nome,
|
|
email: args.email,
|
|
senhaHash,
|
|
roleId: roleTIMaster._id,
|
|
ativo: true,
|
|
primeiroAcesso: true,
|
|
atualizadoEm: Date.now(),
|
|
});
|
|
return {
|
|
sucesso: true as const,
|
|
usuarioId: existentePorMatricula._id,
|
|
senhaTemporaria,
|
|
};
|
|
}
|
|
|
|
// Verificar se email já existe
|
|
const existentePorEmail = await ctx.db
|
|
.query("usuarios")
|
|
.withIndex("by_email", (q) => q.eq("email", args.email))
|
|
.first();
|
|
if (existentePorEmail) {
|
|
// Promove usuário existente por email
|
|
await ctx.db.patch(existentePorEmail._id, {
|
|
matricula: args.matricula,
|
|
nome: args.nome,
|
|
senhaHash,
|
|
roleId: roleTIMaster._id,
|
|
ativo: true,
|
|
primeiroAcesso: true,
|
|
atualizadoEm: Date.now(),
|
|
});
|
|
return {
|
|
sucesso: true as const,
|
|
usuarioId: existentePorEmail._id,
|
|
senhaTemporaria,
|
|
};
|
|
}
|
|
|
|
// Criar novo usuário TI Master
|
|
const usuarioId = await ctx.db.insert("usuarios", {
|
|
matricula: args.matricula,
|
|
senhaHash,
|
|
nome: args.nome,
|
|
email: args.email,
|
|
roleId: roleTIMaster._id,
|
|
ativo: true,
|
|
primeiroAcesso: true,
|
|
criadoEm: Date.now(),
|
|
atualizadoEm: Date.now(),
|
|
});
|
|
|
|
return { sucesso: true as const, usuarioId, senhaTemporaria };
|
|
},
|
|
});
|
|
|
|
/**
|
|
* 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 };
|
|
},
|
|
});
|
|
|
|
/**
|
|
* Criar (ou garantir) um usuário ADMIN padrão
|
|
*/
|
|
export const criarAdminPadrao = mutation({
|
|
args: {
|
|
matricula: v.optional(v.string()),
|
|
nome: v.optional(v.string()),
|
|
email: v.optional(v.string()),
|
|
senha: v.optional(v.string()),
|
|
},
|
|
returns: v.object({
|
|
sucesso: v.boolean(),
|
|
usuarioId: v.optional(v.id("usuarios")),
|
|
}),
|
|
handler: async (ctx, args) => {
|
|
const matricula = args.matricula ?? "0000";
|
|
const nome = args.nome ?? "Administrador Geral";
|
|
const email = args.email ?? "admin@sgse.pe.gov.br";
|
|
const senha = args.senha ?? "Admin@123";
|
|
|
|
// Garantir role ADMIN (nível 2)
|
|
let roleAdmin = await ctx.db
|
|
.query("roles")
|
|
.withIndex("by_nome", (q) => q.eq("nome", "admin"))
|
|
.first();
|
|
if (!roleAdmin) {
|
|
const roleId = await ctx.db.insert("roles", {
|
|
nome: "admin",
|
|
descricao: "Administrador Geral",
|
|
nivel: 2,
|
|
setor: "administrativo",
|
|
customizado: false,
|
|
editavel: true,
|
|
});
|
|
roleAdmin = await ctx.db.get(roleId);
|
|
}
|
|
|
|
if (!roleAdmin) return { sucesso: false };
|
|
|
|
// Verificar se já existe por matrícula ou email
|
|
const existentePorMatricula = await ctx.db
|
|
.query("usuarios")
|
|
.withIndex("by_matricula", (q) => q.eq("matricula", matricula))
|
|
.first();
|
|
|
|
const existentePorEmail = await ctx.db
|
|
.query("usuarios")
|
|
.withIndex("by_email", (q) => q.eq("email", email))
|
|
.first();
|
|
|
|
const senhaHash = await hashPassword(senha);
|
|
|
|
if (existentePorMatricula || existentePorEmail) {
|
|
const alvo = existentePorMatricula ?? existentePorEmail!;
|
|
await ctx.db.patch(alvo._id, {
|
|
matricula,
|
|
nome,
|
|
email,
|
|
senhaHash,
|
|
roleId: roleAdmin._id,
|
|
ativo: true,
|
|
primeiroAcesso: false,
|
|
atualizadoEm: Date.now(),
|
|
});
|
|
return { sucesso: true, usuarioId: alvo._id };
|
|
}
|
|
|
|
const usuarioId = await ctx.db.insert("usuarios", {
|
|
matricula,
|
|
senhaHash,
|
|
nome,
|
|
email,
|
|
roleId: roleAdmin._id,
|
|
ativo: true,
|
|
primeiroAcesso: false,
|
|
criadoEm: Date.now(),
|
|
atualizadoEm: Date.now(),
|
|
});
|
|
|
|
return { sucesso: true, usuarioId };
|
|
},
|
|
});
|