refactor: improve layout and backend monitoring functionality - Streamlined the layout component in Svelte for better readability and consistency. - Enhanced the backend monitoring functions by updating argument structures and improving code clarity. - A #10
2
packages/backend/convex/_generated/api.d.ts
vendored
2
packages/backend/convex/_generated/api.d.ts
vendored
@@ -15,7 +15,6 @@ import type * as actions_smtp from "../actions/smtp.js";
|
|||||||
import type * as actions_utils_nodeCrypto from "../actions/utils/nodeCrypto.js";
|
import type * as actions_utils_nodeCrypto from "../actions/utils/nodeCrypto.js";
|
||||||
import type * as atestadosLicencas from "../atestadosLicencas.js";
|
import type * as atestadosLicencas from "../atestadosLicencas.js";
|
||||||
import type * as ausencias from "../ausencias.js";
|
import type * as ausencias from "../ausencias.js";
|
||||||
import type * as autenticacao from "../autenticacao.js";
|
|
||||||
import type * as auth_utils from "../auth/utils.js";
|
import type * as auth_utils from "../auth/utils.js";
|
||||||
import type * as auth from "../auth.js";
|
import type * as auth from "../auth.js";
|
||||||
import type * as chat from "../chat.js";
|
import type * as chat from "../chat.js";
|
||||||
@@ -71,7 +70,6 @@ declare const fullApi: ApiFromModules<{
|
|||||||
"actions/utils/nodeCrypto": typeof actions_utils_nodeCrypto;
|
"actions/utils/nodeCrypto": typeof actions_utils_nodeCrypto;
|
||||||
atestadosLicencas: typeof atestadosLicencas;
|
atestadosLicencas: typeof atestadosLicencas;
|
||||||
ausencias: typeof ausencias;
|
ausencias: typeof ausencias;
|
||||||
autenticacao: typeof autenticacao;
|
|
||||||
"auth/utils": typeof auth_utils;
|
"auth/utils": typeof auth_utils;
|
||||||
auth: typeof auth;
|
auth: typeof auth;
|
||||||
chat: typeof chat;
|
chat: typeof chat;
|
||||||
|
|||||||
@@ -1,834 +0,0 @@
|
|||||||
import { v } from "convex/values";
|
|
||||||
import { mutation, query, internalMutation } from "./_generated/server";
|
|
||||||
import {
|
|
||||||
hashPassword,
|
|
||||||
verifyPassword,
|
|
||||||
generateToken,
|
|
||||||
validarMatricula,
|
|
||||||
validarSenha,
|
|
||||||
} from "./auth/utils";
|
|
||||||
import { registrarLogin } from "./logsLogin";
|
|
||||||
import { Id, Doc } from "./_generated/dataModel";
|
|
||||||
import type { QueryCtx, MutationCtx } from "./_generated/server";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper para verificar se usuário está bloqueado
|
|
||||||
*/
|
|
||||||
async function verificarBloqueioUsuario(ctx: QueryCtx, usuarioId: Id<"usuarios">) {
|
|
||||||
const bloqueio = await ctx.db
|
|
||||||
.query("bloqueiosUsuarios")
|
|
||||||
.withIndex("by_usuario", (q) => q.eq("usuarioId", usuarioId))
|
|
||||||
.filter((q) => q.eq(q.field("ativo"), true))
|
|
||||||
.first();
|
|
||||||
|
|
||||||
return bloqueio !== null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper para verificar rate limiting por IP
|
|
||||||
*/
|
|
||||||
async function verificarRateLimitIP(ctx: QueryCtx, ipAddress: string) {
|
|
||||||
// Últimas 15 minutos
|
|
||||||
const dataLimite = Date.now() - 15 * 60 * 1000;
|
|
||||||
|
|
||||||
const tentativas = await ctx.db
|
|
||||||
.query("logsLogin")
|
|
||||||
.withIndex("by_ip", (q) => q.eq("ipAddress", ipAddress))
|
|
||||||
.filter((q) => q.gte(q.field("timestamp"), dataLimite))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
const falhas = tentativas.filter((t) => !t.sucesso).length;
|
|
||||||
|
|
||||||
// Bloquear se 5 ou mais tentativas falhas em 15 minutos
|
|
||||||
return falhas >= 5;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Login do usuário (aceita matrícula OU email)
|
|
||||||
*/
|
|
||||||
export const login = mutation({
|
|
||||||
args: {
|
|
||||||
matriculaOuEmail: v.string(), // Aceita matrícula ou email
|
|
||||||
senha: v.string(),
|
|
||||||
ipAddress: v.optional(v.string()),
|
|
||||||
userAgent: v.optional(v.string()),
|
|
||||||
},
|
|
||||||
returns: v.union(
|
|
||||||
v.object({
|
|
||||||
sucesso: v.literal(true),
|
|
||||||
token: v.string(),
|
|
||||||
usuario: v.object({
|
|
||||||
_id: v.id("usuarios"),
|
|
||||||
matricula: v.string(),
|
|
||||||
nome: v.string(),
|
|
||||||
email: v.string(),
|
|
||||||
funcionarioId: v.optional(v.id("funcionarios")),
|
|
||||||
role: v.object({
|
|
||||||
_id: v.id("roles"),
|
|
||||||
nome: v.string(),
|
|
||||||
nivel: v.number(),
|
|
||||||
setor: v.optional(v.string()),
|
|
||||||
}),
|
|
||||||
primeiroAcesso: v.boolean(),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
v.object({
|
|
||||||
sucesso: v.literal(false),
|
|
||||||
erro: v.string(),
|
|
||||||
})
|
|
||||||
),
|
|
||||||
handler: async (ctx, args) => {
|
|
||||||
// Verificar rate limiting por IP
|
|
||||||
if (args.ipAddress) {
|
|
||||||
const ipBloqueado = await verificarRateLimitIP(ctx, args.ipAddress);
|
|
||||||
if (ipBloqueado) {
|
|
||||||
await registrarLogin(ctx, {
|
|
||||||
matriculaOuEmail: args.matriculaOuEmail,
|
|
||||||
sucesso: false,
|
|
||||||
motivoFalha: "rate_limit_excedido",
|
|
||||||
ipAddress: args.ipAddress,
|
|
||||||
userAgent: args.userAgent,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
sucesso: false as const,
|
|
||||||
erro: "Muitas tentativas de login. Tente novamente em 15 minutos.",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determinar se é email ou matrícula
|
|
||||||
const isEmail = args.matriculaOuEmail.includes("@");
|
|
||||||
|
|
||||||
// Buscar usuário
|
|
||||||
let usuario: Doc<"usuarios"> | null = null;
|
|
||||||
if (isEmail) {
|
|
||||||
usuario = await ctx.db
|
|
||||||
.query("usuarios")
|
|
||||||
.withIndex("by_email", (q) => q.eq("email", args.matriculaOuEmail))
|
|
||||||
.first();
|
|
||||||
} else {
|
|
||||||
const funcionario: Doc<"funcionarios"> | null = await ctx.db.query("funcionarios").withIndex("by_matricula", (q) => q.eq("matricula", args.matriculaOuEmail)).first();
|
|
||||||
if (funcionario) {
|
|
||||||
usuario = await ctx.db
|
|
||||||
.query("usuarios")
|
|
||||||
.withIndex("by_funcionarioId", (q) => q.eq("funcionarioId", funcionario._id))
|
|
||||||
.first();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!usuario) {
|
|
||||||
await registrarLogin(ctx, {
|
|
||||||
matriculaOuEmail: args.matriculaOuEmail,
|
|
||||||
sucesso: false,
|
|
||||||
motivoFalha: "usuario_inexistente",
|
|
||||||
ipAddress: args.ipAddress,
|
|
||||||
userAgent: args.userAgent,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
sucesso: false as const,
|
|
||||||
erro: "Credenciais incorretas.",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verificar se usuário está bloqueado
|
|
||||||
if (
|
|
||||||
usuario.bloqueado ||
|
|
||||||
(await verificarBloqueioUsuario(ctx, usuario._id))
|
|
||||||
) {
|
|
||||||
await registrarLogin(ctx, {
|
|
||||||
usuarioId: usuario._id,
|
|
||||||
matriculaOuEmail: args.matriculaOuEmail,
|
|
||||||
sucesso: false,
|
|
||||||
motivoFalha: "usuario_bloqueado",
|
|
||||||
ipAddress: args.ipAddress,
|
|
||||||
userAgent: args.userAgent,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
sucesso: false as const,
|
|
||||||
erro: "Usuário bloqueado. Entre em contato com o TI.",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verificar se usuário está ativo
|
|
||||||
if (!usuario.ativo) {
|
|
||||||
await registrarLogin(ctx, {
|
|
||||||
usuarioId: usuario._id,
|
|
||||||
matriculaOuEmail: args.matriculaOuEmail,
|
|
||||||
sucesso: false,
|
|
||||||
motivoFalha: "usuario_inativo",
|
|
||||||
ipAddress: args.ipAddress,
|
|
||||||
userAgent: args.userAgent,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
sucesso: false as const,
|
|
||||||
erro: "Usuário inativo. Entre em contato com o TI.",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verificar tentativas de login (bloqueio temporário)
|
|
||||||
const tentativasRecentes = usuario.tentativasLogin || 0;
|
|
||||||
const ultimaTentativa = usuario.ultimaTentativaLogin || 0;
|
|
||||||
const tempoDecorrido = Date.now() - ultimaTentativa;
|
|
||||||
const TEMPO_BLOQUEIO = 30 * 60 * 1000; // 30 minutos
|
|
||||||
|
|
||||||
// Se tentou 5 vezes e ainda não passou o tempo de bloqueio
|
|
||||||
if (tentativasRecentes >= 5 && tempoDecorrido < TEMPO_BLOQUEIO) {
|
|
||||||
await registrarLogin(ctx, {
|
|
||||||
usuarioId: usuario._id,
|
|
||||||
matriculaOuEmail: args.matriculaOuEmail,
|
|
||||||
sucesso: false,
|
|
||||||
motivoFalha: "bloqueio_temporario",
|
|
||||||
ipAddress: args.ipAddress,
|
|
||||||
userAgent: args.userAgent,
|
|
||||||
});
|
|
||||||
|
|
||||||
const minutosRestantes = Math.ceil(
|
|
||||||
(TEMPO_BLOQUEIO - tempoDecorrido) / 60000
|
|
||||||
);
|
|
||||||
return {
|
|
||||||
sucesso: false as const,
|
|
||||||
erro: `Conta temporariamente bloqueada. Tente novamente em ${minutosRestantes} minutos.`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resetar tentativas se passou o tempo de bloqueio
|
|
||||||
if (tempoDecorrido > TEMPO_BLOQUEIO) {
|
|
||||||
await ctx.db.patch(usuario._id, {
|
|
||||||
tentativasLogin: 0,
|
|
||||||
ultimaTentativaLogin: Date.now(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verificar senha
|
|
||||||
const senhaValida = await verifyPassword(args.senha, usuario.senhaHash);
|
|
||||||
|
|
||||||
if (!senhaValida) {
|
|
||||||
// Incrementar tentativas
|
|
||||||
const novasTentativas =
|
|
||||||
tempoDecorrido > TEMPO_BLOQUEIO ? 1 : tentativasRecentes + 1;
|
|
||||||
|
|
||||||
await ctx.db.patch(usuario._id, {
|
|
||||||
tentativasLogin: novasTentativas,
|
|
||||||
ultimaTentativaLogin: Date.now(),
|
|
||||||
});
|
|
||||||
|
|
||||||
await registrarLogin(ctx, {
|
|
||||||
usuarioId: usuario._id,
|
|
||||||
matriculaOuEmail: args.matriculaOuEmail,
|
|
||||||
sucesso: false,
|
|
||||||
motivoFalha: "senha_incorreta",
|
|
||||||
ipAddress: args.ipAddress,
|
|
||||||
userAgent: args.userAgent,
|
|
||||||
});
|
|
||||||
|
|
||||||
const tentativasRestantes = 5 - novasTentativas;
|
|
||||||
if (tentativasRestantes > 0) {
|
|
||||||
return {
|
|
||||||
sucesso: false as const,
|
|
||||||
erro: `Credenciais incorretas. ${tentativasRestantes} tentativas restantes.`,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
return {
|
|
||||||
sucesso: false as const,
|
|
||||||
erro: "Conta bloqueada por 30 minutos devido a múltiplas tentativas falhas.",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Login bem-sucedido! Resetar tentativas
|
|
||||||
await ctx.db.patch(usuario._id, {
|
|
||||||
tentativasLogin: 0,
|
|
||||||
ultimaTentativaLogin: undefined,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Buscar role do usuário
|
|
||||||
const role: Doc<"roles"> | null = await ctx.db.get(usuario.roleId);
|
|
||||||
if (!role) {
|
|
||||||
return {
|
|
||||||
sucesso: false as const,
|
|
||||||
erro: "Erro ao carregar permissões do usuário.",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Gerar token de sessão
|
|
||||||
const token = generateToken();
|
|
||||||
const agora = Date.now();
|
|
||||||
const expiraEm = agora + 8 * 60 * 60 * 1000; // 8 horas
|
|
||||||
|
|
||||||
// Criar sessão
|
|
||||||
await ctx.db.insert("sessoes", {
|
|
||||||
usuarioId: usuario._id,
|
|
||||||
token,
|
|
||||||
ipAddress: args.ipAddress,
|
|
||||||
userAgent: args.userAgent,
|
|
||||||
criadoEm: agora,
|
|
||||||
expiraEm,
|
|
||||||
ativo: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Atualizar último acesso
|
|
||||||
await ctx.db.patch(usuario._id, {
|
|
||||||
ultimoAcesso: agora,
|
|
||||||
atualizadoEm: agora,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Log de login bem-sucedido
|
|
||||||
await registrarLogin(ctx, {
|
|
||||||
usuarioId: usuario._id,
|
|
||||||
matriculaOuEmail: args.matriculaOuEmail,
|
|
||||||
sucesso: true,
|
|
||||||
ipAddress: args.ipAddress,
|
|
||||||
userAgent: args.userAgent,
|
|
||||||
});
|
|
||||||
|
|
||||||
await ctx.db.insert("logsAcesso", {
|
|
||||||
usuarioId: usuario._id,
|
|
||||||
tipo: "login",
|
|
||||||
ipAddress: args.ipAddress,
|
|
||||||
userAgent: args.userAgent,
|
|
||||||
detalhes: "Login realizado com sucesso",
|
|
||||||
timestamp: agora,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Obter matrícula do funcionário se houver
|
|
||||||
let matricula: string | undefined = undefined;
|
|
||||||
if (usuario.funcionarioId) {
|
|
||||||
const funcionario = await ctx.db.get(usuario.funcionarioId);
|
|
||||||
matricula = funcionario?.matricula;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
sucesso: true as const,
|
|
||||||
token,
|
|
||||||
usuario: {
|
|
||||||
_id: usuario._id,
|
|
||||||
matricula: matricula || "",
|
|
||||||
nome: usuario.nome,
|
|
||||||
email: usuario.email,
|
|
||||||
funcionarioId: usuario.funcionarioId,
|
|
||||||
role: {
|
|
||||||
_id: role._id,
|
|
||||||
nome: role.nome,
|
|
||||||
nivel: role.nivel,
|
|
||||||
setor: role.setor,
|
|
||||||
},
|
|
||||||
primeiroAcesso: usuario.primeiroAcesso,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Mutation interna para login via HTTP (com IP extraído do request)
|
|
||||||
* Usada pelo endpoint HTTP /api/login
|
|
||||||
*/
|
|
||||||
export const loginComIP = internalMutation({
|
|
||||||
args: {
|
|
||||||
matriculaOuEmail: v.string(),
|
|
||||||
senha: v.string(),
|
|
||||||
ipAddress: v.optional(v.string()),
|
|
||||||
userAgent: v.optional(v.string()),
|
|
||||||
},
|
|
||||||
returns: v.union(
|
|
||||||
v.object({
|
|
||||||
sucesso: v.literal(true),
|
|
||||||
token: v.string(),
|
|
||||||
usuario: v.object({
|
|
||||||
_id: v.id("usuarios"),
|
|
||||||
matricula: v.string(),
|
|
||||||
nome: v.string(),
|
|
||||||
email: v.string(),
|
|
||||||
funcionarioId: v.optional(v.id("funcionarios")),
|
|
||||||
role: v.object({
|
|
||||||
_id: v.id("roles"),
|
|
||||||
nome: v.string(),
|
|
||||||
nivel: v.number(),
|
|
||||||
setor: v.optional(v.string()),
|
|
||||||
}),
|
|
||||||
primeiroAcesso: v.boolean(),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
v.object({
|
|
||||||
sucesso: v.literal(false),
|
|
||||||
erro: v.string(),
|
|
||||||
})
|
|
||||||
),
|
|
||||||
handler: async (ctx, args) => {
|
|
||||||
// Reutilizar a mesma lógica da mutation pública
|
|
||||||
// Verificar rate limiting por IP
|
|
||||||
if (args.ipAddress) {
|
|
||||||
const ipBloqueado = await verificarRateLimitIP(ctx, args.ipAddress);
|
|
||||||
if (ipBloqueado) {
|
|
||||||
await registrarLogin(ctx, {
|
|
||||||
matriculaOuEmail: args.matriculaOuEmail,
|
|
||||||
sucesso: false,
|
|
||||||
motivoFalha: "rate_limit_excedido",
|
|
||||||
ipAddress: args.ipAddress,
|
|
||||||
userAgent: args.userAgent,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
sucesso: false as const,
|
|
||||||
erro: "Muitas tentativas de login. Tente novamente em 15 minutos.",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determinar se é email ou matrícula
|
|
||||||
const isEmail = args.matriculaOuEmail.includes("@");
|
|
||||||
|
|
||||||
// Buscar usuário
|
|
||||||
let usuario: Doc<"usuarios"> | null = null;
|
|
||||||
if (isEmail) {
|
|
||||||
usuario = await ctx.db
|
|
||||||
.query("usuarios")
|
|
||||||
.withIndex("by_email", (q) => q.eq("email", args.matriculaOuEmail))
|
|
||||||
.first();
|
|
||||||
} else {
|
|
||||||
const funcionario: Doc<"funcionarios"> | null = await ctx.db.query("funcionarios").withIndex("by_matricula", (q) => q.eq("matricula", args.matriculaOuEmail)).first();
|
|
||||||
if (funcionario) {
|
|
||||||
usuario = await ctx.db
|
|
||||||
.query("usuarios")
|
|
||||||
.withIndex("by_funcionarioId", (q) => q.eq("funcionarioId", funcionario._id))
|
|
||||||
.first();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!usuario) {
|
|
||||||
await registrarLogin(ctx, {
|
|
||||||
matriculaOuEmail: args.matriculaOuEmail,
|
|
||||||
sucesso: false,
|
|
||||||
motivoFalha: "usuario_inexistente",
|
|
||||||
ipAddress: args.ipAddress,
|
|
||||||
userAgent: args.userAgent,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
sucesso: false as const,
|
|
||||||
erro: "Credenciais incorretas.",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verificar se usuário está bloqueado
|
|
||||||
if (
|
|
||||||
usuario.bloqueado ||
|
|
||||||
(await verificarBloqueioUsuario(ctx, usuario._id))
|
|
||||||
) {
|
|
||||||
await registrarLogin(ctx, {
|
|
||||||
usuarioId: usuario._id,
|
|
||||||
matriculaOuEmail: args.matriculaOuEmail,
|
|
||||||
sucesso: false,
|
|
||||||
motivoFalha: "usuario_bloqueado",
|
|
||||||
ipAddress: args.ipAddress,
|
|
||||||
userAgent: args.userAgent,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
sucesso: false as const,
|
|
||||||
erro: "Usuário bloqueado. Entre em contato com o TI.",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verificar se usuário está ativo
|
|
||||||
if (!usuario.ativo) {
|
|
||||||
await registrarLogin(ctx, {
|
|
||||||
usuarioId: usuario._id,
|
|
||||||
matriculaOuEmail: args.matriculaOuEmail,
|
|
||||||
sucesso: false,
|
|
||||||
motivoFalha: "usuario_inativo",
|
|
||||||
ipAddress: args.ipAddress,
|
|
||||||
userAgent: args.userAgent,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
sucesso: false as const,
|
|
||||||
erro: "Usuário inativo. Entre em contato com o TI.",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verificar tentativas de login (bloqueio temporário)
|
|
||||||
const tentativasRecentes = usuario.tentativasLogin || 0;
|
|
||||||
const ultimaTentativa = usuario.ultimaTentativaLogin || 0;
|
|
||||||
const tempoDecorrido = Date.now() - ultimaTentativa;
|
|
||||||
const TEMPO_BLOQUEIO = 30 * 60 * 1000; // 30 minutos
|
|
||||||
|
|
||||||
// Se tentou 5 vezes e ainda não passou o tempo de bloqueio
|
|
||||||
if (tentativasRecentes >= 5 && tempoDecorrido < TEMPO_BLOQUEIO) {
|
|
||||||
await registrarLogin(ctx, {
|
|
||||||
usuarioId: usuario._id,
|
|
||||||
matriculaOuEmail: args.matriculaOuEmail,
|
|
||||||
sucesso: false,
|
|
||||||
motivoFalha: "bloqueio_temporario",
|
|
||||||
ipAddress: args.ipAddress,
|
|
||||||
userAgent: args.userAgent,
|
|
||||||
});
|
|
||||||
|
|
||||||
const minutosRestantes = Math.ceil(
|
|
||||||
(TEMPO_BLOQUEIO - tempoDecorrido) / 60000
|
|
||||||
);
|
|
||||||
return {
|
|
||||||
sucesso: false as const,
|
|
||||||
erro: `Conta temporariamente bloqueada. Tente novamente em ${minutosRestantes} minutos.`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resetar tentativas se passou o tempo de bloqueio
|
|
||||||
if (tempoDecorrido > TEMPO_BLOQUEIO) {
|
|
||||||
await ctx.db.patch(usuario._id, {
|
|
||||||
tentativasLogin: 0,
|
|
||||||
ultimaTentativaLogin: Date.now(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verificar senha
|
|
||||||
const senhaValida = await verifyPassword(args.senha, usuario.senhaHash);
|
|
||||||
|
|
||||||
if (!senhaValida) {
|
|
||||||
// Incrementar tentativas
|
|
||||||
const novasTentativas =
|
|
||||||
tempoDecorrido > TEMPO_BLOQUEIO ? 1 : tentativasRecentes + 1;
|
|
||||||
|
|
||||||
await ctx.db.patch(usuario._id, {
|
|
||||||
tentativasLogin: novasTentativas,
|
|
||||||
ultimaTentativaLogin: Date.now(),
|
|
||||||
});
|
|
||||||
|
|
||||||
await registrarLogin(ctx, {
|
|
||||||
usuarioId: usuario._id,
|
|
||||||
matriculaOuEmail: args.matriculaOuEmail,
|
|
||||||
sucesso: false,
|
|
||||||
motivoFalha: "senha_incorreta",
|
|
||||||
ipAddress: args.ipAddress,
|
|
||||||
userAgent: args.userAgent,
|
|
||||||
});
|
|
||||||
|
|
||||||
const tentativasRestantes = 5 - novasTentativas;
|
|
||||||
if (tentativasRestantes > 0) {
|
|
||||||
return {
|
|
||||||
sucesso: false as const,
|
|
||||||
erro: `Credenciais incorretas. ${tentativasRestantes} tentativas restantes.`,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
return {
|
|
||||||
sucesso: false as const,
|
|
||||||
erro: "Conta bloqueada por 30 minutos devido a múltiplas tentativas falhas.",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Login bem-sucedido! Resetar tentativas
|
|
||||||
await ctx.db.patch(usuario._id, {
|
|
||||||
tentativasLogin: 0,
|
|
||||||
ultimaTentativaLogin: undefined,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Buscar role do usuário
|
|
||||||
const role: Doc<"roles"> | null = await ctx.db.get(usuario.roleId);
|
|
||||||
if (!role) {
|
|
||||||
return {
|
|
||||||
sucesso: false as const,
|
|
||||||
erro: "Erro ao carregar permissões do usuário.",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Gerar token de sessão
|
|
||||||
const token = generateToken();
|
|
||||||
const agora = Date.now();
|
|
||||||
const expiraEm = agora + 8 * 60 * 60 * 1000; // 8 horas
|
|
||||||
|
|
||||||
// Criar sessão
|
|
||||||
await ctx.db.insert("sessoes", {
|
|
||||||
usuarioId: usuario._id,
|
|
||||||
token,
|
|
||||||
ipAddress: args.ipAddress,
|
|
||||||
userAgent: args.userAgent,
|
|
||||||
criadoEm: agora,
|
|
||||||
expiraEm,
|
|
||||||
ativo: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Atualizar último acesso
|
|
||||||
await ctx.db.patch(usuario._id, {
|
|
||||||
ultimoAcesso: agora,
|
|
||||||
atualizadoEm: agora,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Log de login bem-sucedido
|
|
||||||
await registrarLogin(ctx, {
|
|
||||||
usuarioId: usuario._id,
|
|
||||||
matriculaOuEmail: args.matriculaOuEmail,
|
|
||||||
sucesso: true,
|
|
||||||
ipAddress: args.ipAddress,
|
|
||||||
userAgent: args.userAgent,
|
|
||||||
});
|
|
||||||
|
|
||||||
await ctx.db.insert("logsAcesso", {
|
|
||||||
usuarioId: usuario._id,
|
|
||||||
tipo: "login",
|
|
||||||
ipAddress: args.ipAddress,
|
|
||||||
userAgent: args.userAgent,
|
|
||||||
detalhes: "Login realizado com sucesso",
|
|
||||||
timestamp: agora,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Obter matrícula do funcionário se houver
|
|
||||||
let matricula: string | undefined = undefined;
|
|
||||||
if (usuario.funcionarioId) {
|
|
||||||
const funcionario = await ctx.db.get(usuario.funcionarioId);
|
|
||||||
matricula = funcionario?.matricula;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
sucesso: true as const,
|
|
||||||
token,
|
|
||||||
usuario: {
|
|
||||||
_id: usuario._id,
|
|
||||||
matricula: matricula || "",
|
|
||||||
nome: usuario.nome,
|
|
||||||
email: usuario.email,
|
|
||||||
funcionarioId: usuario.funcionarioId,
|
|
||||||
role: {
|
|
||||||
_id: role._id,
|
|
||||||
nome: role.nome,
|
|
||||||
nivel: role.nivel,
|
|
||||||
setor: role.setor,
|
|
||||||
},
|
|
||||||
primeiroAcesso: usuario.primeiroAcesso,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Logout do usuário
|
|
||||||
*/
|
|
||||||
export const logout = mutation({
|
|
||||||
args: {
|
|
||||||
token: v.string(),
|
|
||||||
},
|
|
||||||
returns: v.null(),
|
|
||||||
handler: async (ctx, args) => {
|
|
||||||
// Buscar sessão
|
|
||||||
const sessao = await ctx.db
|
|
||||||
.query("sessoes")
|
|
||||||
.withIndex("by_token", (q) => q.eq("token", args.token))
|
|
||||||
.first();
|
|
||||||
|
|
||||||
if (sessao) {
|
|
||||||
// Desativar sessão
|
|
||||||
await ctx.db.patch(sessao._id, {
|
|
||||||
ativo: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Log de logout
|
|
||||||
await ctx.db.insert("logsAcesso", {
|
|
||||||
usuarioId: sessao.usuarioId,
|
|
||||||
tipo: "logout",
|
|
||||||
timestamp: Date.now(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Verificar se token é válido e retornar usuário
|
|
||||||
*/
|
|
||||||
export const verificarSessao = query({
|
|
||||||
args: {
|
|
||||||
token: v.string(),
|
|
||||||
},
|
|
||||||
returns: v.union(
|
|
||||||
v.object({
|
|
||||||
valido: v.literal(true),
|
|
||||||
usuario: v.object({
|
|
||||||
_id: v.id("usuarios"),
|
|
||||||
matricula: v.string(),
|
|
||||||
nome: v.string(),
|
|
||||||
email: v.string(),
|
|
||||||
funcionarioId: v.optional(v.id("funcionarios")),
|
|
||||||
role: v.object({
|
|
||||||
_id: v.id("roles"),
|
|
||||||
nome: v.string(),
|
|
||||||
nivel: v.number(),
|
|
||||||
setor: v.optional(v.string()),
|
|
||||||
}),
|
|
||||||
primeiroAcesso: v.boolean(),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
v.object({
|
|
||||||
valido: v.literal(false),
|
|
||||||
motivo: v.optional(v.string()),
|
|
||||||
})
|
|
||||||
),
|
|
||||||
handler: async (ctx, args) => {
|
|
||||||
// Buscar sessão
|
|
||||||
const sessao: Doc<"sessoes"> | null = await ctx.db
|
|
||||||
.query("sessoes")
|
|
||||||
.withIndex("by_token", (q) => q.eq("token", args.token))
|
|
||||||
.first();
|
|
||||||
|
|
||||||
if (!sessao || !sessao.ativo) {
|
|
||||||
return {
|
|
||||||
valido: false as const,
|
|
||||||
motivo: "Sessão não encontrada ou inativa",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verificar se sessão expirou
|
|
||||||
if (sessao.expiraEm < Date.now()) {
|
|
||||||
// Não podemos fazer patch/insert em uma query
|
|
||||||
// A expiração será tratada por uma mutation separada
|
|
||||||
return { valido: false as const, motivo: "Sessão expirada" };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Buscar usuário
|
|
||||||
const usuario: Doc<"usuarios"> | null = await ctx.db.get(sessao.usuarioId);
|
|
||||||
if (!usuario || !usuario.ativo) {
|
|
||||||
return {
|
|
||||||
valido: false as const,
|
|
||||||
motivo: "Usuário não encontrado ou inativo",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Buscar role
|
|
||||||
const role: Doc<"roles"> | null = await ctx.db.get(usuario.roleId);
|
|
||||||
if (!role) {
|
|
||||||
return { valido: false as const, motivo: "Role não encontrada" };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Obter matrícula do funcionário se houver
|
|
||||||
let matricula: string | undefined = undefined;
|
|
||||||
if (usuario.funcionarioId) {
|
|
||||||
const funcionario = await ctx.db.get(usuario.funcionarioId);
|
|
||||||
matricula = funcionario?.matricula;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
valido: true as const,
|
|
||||||
usuario: {
|
|
||||||
_id: usuario._id,
|
|
||||||
matricula: matricula || "",
|
|
||||||
nome: usuario.nome,
|
|
||||||
email: usuario.email,
|
|
||||||
funcionarioId: usuario.funcionarioId,
|
|
||||||
role: {
|
|
||||||
_id: role._id,
|
|
||||||
nome: role.nome,
|
|
||||||
nivel: role.nivel,
|
|
||||||
setor: role.setor,
|
|
||||||
},
|
|
||||||
primeiroAcesso: usuario.primeiroAcesso,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Limpar sessões expiradas (chamada periodicamente)
|
|
||||||
*/
|
|
||||||
export const limparSessoesExpiradas = mutation({
|
|
||||||
args: {},
|
|
||||||
returns: v.object({
|
|
||||||
sessoesLimpas: v.number(),
|
|
||||||
}),
|
|
||||||
handler: async (ctx) => {
|
|
||||||
const agora = Date.now();
|
|
||||||
const sessoes = await ctx.db
|
|
||||||
.query("sessoes")
|
|
||||||
.withIndex("by_ativo", (q) => q.eq("ativo", true))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let sessoesLimpas = 0;
|
|
||||||
|
|
||||||
for (const sessao of sessoes) {
|
|
||||||
if (sessao.expiraEm < agora) {
|
|
||||||
await ctx.db.patch(sessao._id, { ativo: false });
|
|
||||||
|
|
||||||
await ctx.db.insert("logsAcesso", {
|
|
||||||
usuarioId: sessao.usuarioId,
|
|
||||||
tipo: "sessao_expirada",
|
|
||||||
timestamp: agora,
|
|
||||||
});
|
|
||||||
|
|
||||||
sessoesLimpas++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { sessoesLimpas };
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Alterar senha (primeiro acesso ou reset)
|
|
||||||
*/
|
|
||||||
export const alterarSenha = mutation({
|
|
||||||
args: {
|
|
||||||
token: v.string(),
|
|
||||||
senhaAtual: v.optional(v.string()),
|
|
||||||
novaSenha: v.string(),
|
|
||||||
},
|
|
||||||
returns: v.union(
|
|
||||||
v.object({ sucesso: v.literal(true) }),
|
|
||||||
v.object({ sucesso: v.literal(false), erro: v.string() })
|
|
||||||
),
|
|
||||||
handler: async (ctx, args) => {
|
|
||||||
// Verificar sessão
|
|
||||||
const sessao: Doc<"sessoes"> | null = await ctx.db
|
|
||||||
.query("sessoes")
|
|
||||||
.withIndex("by_token", (q) => q.eq("token", args.token))
|
|
||||||
.first();
|
|
||||||
|
|
||||||
if (!sessao || !sessao.ativo) {
|
|
||||||
return { sucesso: false as const, erro: "Sessão inválida" };
|
|
||||||
}
|
|
||||||
|
|
||||||
const usuario: Doc<"usuarios"> | null = await ctx.db.get(sessao.usuarioId);
|
|
||||||
if (!usuario) {
|
|
||||||
return { sucesso: false as const, erro: "Usuário não encontrado" };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Se não for primeiro acesso, verificar senha atual
|
|
||||||
if (!usuario.primeiroAcesso && args.senhaAtual) {
|
|
||||||
const senhaAtualValida = await verifyPassword(
|
|
||||||
args.senhaAtual,
|
|
||||||
usuario.senhaHash
|
|
||||||
);
|
|
||||||
if (!senhaAtualValida) {
|
|
||||||
return { sucesso: false as const, erro: "Senha atual incorreta" };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validar nova senha
|
|
||||||
if (!validarSenha(args.novaSenha)) {
|
|
||||||
return {
|
|
||||||
sucesso: false as const,
|
|
||||||
erro: "Senha deve ter no mínimo 8 caracteres, incluindo letras, números e símbolos",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Gerar hash da nova senha
|
|
||||||
const novoHash = await hashPassword(args.novaSenha);
|
|
||||||
|
|
||||||
// Atualizar senha
|
|
||||||
await ctx.db.patch(usuario._id, {
|
|
||||||
senhaHash: novoHash,
|
|
||||||
primeiroAcesso: false,
|
|
||||||
atualizadoEm: Date.now(),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Log
|
|
||||||
await ctx.db.insert("logsAcesso", {
|
|
||||||
usuarioId: usuario._id,
|
|
||||||
tipo: "senha_alterada",
|
|
||||||
timestamp: Date.now(),
|
|
||||||
});
|
|
||||||
|
|
||||||
return { sucesso: true as const };
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -2,8 +2,9 @@ import { createClient, type GenericCtx } from "@convex-dev/better-auth";
|
|||||||
import { convex } from "@convex-dev/better-auth/plugins";
|
import { convex } from "@convex-dev/better-auth/plugins";
|
||||||
import { components } from "./_generated/api";
|
import { components } from "./_generated/api";
|
||||||
import { type DataModel } from "./_generated/dataModel";
|
import { type DataModel } from "./_generated/dataModel";
|
||||||
import { query } from "./_generated/server";
|
import { mutation, MutationCtx, query, QueryCtx } from "./_generated/server";
|
||||||
import { betterAuth } from "better-auth";
|
import { betterAuth } from "better-auth";
|
||||||
|
import { v } from "convex/values";
|
||||||
|
|
||||||
const siteUrl = process.env.SITE_URL!;
|
const siteUrl = process.env.SITE_URL!;
|
||||||
|
|
||||||
@@ -55,3 +56,52 @@ export const getCurrentUser = query({
|
|||||||
return user;
|
return user;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const getCurrentUserFunction = async (ctx: QueryCtx | MutationCtx) => {
|
||||||
|
const authUser = await authComponent.safeGetAuthUser(ctx as any);
|
||||||
|
if (!authUser) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await ctx.db
|
||||||
|
.query("usuarios")
|
||||||
|
.withIndex("authId", (q) => q.eq("authId", authUser._id))
|
||||||
|
.unique();
|
||||||
|
if (!user) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return user;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createAuthUser = async (
|
||||||
|
ctx: MutationCtx,
|
||||||
|
args: { nome: string; email: string; password: string }
|
||||||
|
) => {
|
||||||
|
const { auth, headers } = await authComponent.getAuth(createAuth, ctx as any);
|
||||||
|
|
||||||
|
const result = await auth.api.signUpEmail({
|
||||||
|
headers,
|
||||||
|
body: {
|
||||||
|
name: args.nome,
|
||||||
|
email: args.email,
|
||||||
|
password: args.password,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return result.user.id;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updatePassword = async (
|
||||||
|
ctx: MutationCtx,
|
||||||
|
args: { newPassword: string; currentPassword: string }
|
||||||
|
) => {
|
||||||
|
const { auth, headers } = await authComponent.getAuth(createAuth, ctx as any);
|
||||||
|
|
||||||
|
await auth.api.changePassword({
|
||||||
|
headers,
|
||||||
|
body: {
|
||||||
|
currentPassword: args.currentPassword,
|
||||||
|
newPassword: args.newPassword,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -381,8 +381,7 @@ export default defineSchema({
|
|||||||
|
|
||||||
// Sistema de Autenticação e Controle de Acesso
|
// Sistema de Autenticação e Controle de Acesso
|
||||||
usuarios: defineTable({
|
usuarios: defineTable({
|
||||||
authId: v.optional(v.string()),
|
authId: v.string(),
|
||||||
senhaHash: v.string(), // Senha criptografada com bcrypt
|
|
||||||
nome: v.string(),
|
nome: v.string(),
|
||||||
email: v.string(),
|
email: v.string(),
|
||||||
funcionarioId: v.optional(v.id("funcionarios")),
|
funcionarioId: v.optional(v.id("funcionarios")),
|
||||||
|
|||||||
@@ -1,8 +1,15 @@
|
|||||||
import { internalMutation, mutation, query } from "./_generated/server";
|
import {
|
||||||
|
action,
|
||||||
|
internalAction,
|
||||||
|
internalMutation,
|
||||||
|
mutation,
|
||||||
|
query,
|
||||||
|
} from "./_generated/server";
|
||||||
import { internal } from "./_generated/api";
|
import { internal } from "./_generated/api";
|
||||||
import { v } from "convex/values";
|
import { v } from "convex/values";
|
||||||
import { hashPassword } from "./auth/utils";
|
import { hashPassword } from "./auth/utils";
|
||||||
import { Id } from "./_generated/dataModel";
|
import { Id } from "./_generated/dataModel";
|
||||||
|
import { createAuthUser } from "./auth";
|
||||||
|
|
||||||
// Dados exportados do Convex Cloud
|
// Dados exportados do Convex Cloud
|
||||||
const simbolosData = [
|
const simbolosData = [
|
||||||
@@ -187,83 +194,63 @@ const solicitacoesAcessoData = [
|
|||||||
/**
|
/**
|
||||||
* Seed inicial do banco de dados com os dados exportados do Convex Cloud
|
* Seed inicial do banco de dados com os dados exportados do Convex Cloud
|
||||||
*/
|
*/
|
||||||
export const seedDatabase = internalMutation({
|
export const seedCreateRoles = internalMutation({
|
||||||
args: {},
|
args: {},
|
||||||
returns: v.null(),
|
returns: v.null(),
|
||||||
handler: async (ctx) => {
|
handler: async (ctx) => {
|
||||||
console.log("🌱 Iniciando seed do banco de dados...");
|
|
||||||
|
|
||||||
// 1. Criar Roles (Perfis de Acesso)
|
|
||||||
console.log("🔐 Criando roles...");
|
console.log("🔐 Criando roles...");
|
||||||
// TI_MASTER - Nível 0 - Acesso total irrestrito
|
const ensureRole = async (
|
||||||
const roleTIMaster = await ctx.db.insert("roles", {
|
nome: string,
|
||||||
nome: "ti_master",
|
descricao: string,
|
||||||
descricao: "TI Master",
|
nivel: number,
|
||||||
nivel: 0,
|
setor?: string,
|
||||||
setor: "ti",
|
editavel?: boolean
|
||||||
customizado: false,
|
) => {
|
||||||
editavel: false,
|
const existing = await ctx.db
|
||||||
});
|
.query("roles")
|
||||||
console.log(" ✅ Role criada: ti_master (Nível 0 - Acesso Total)");
|
.withIndex("by_nome", (q) => q.eq("nome", nome))
|
||||||
|
.first();
|
||||||
|
if (existing) {
|
||||||
|
console.log(` ℹ️ Role já existe: ${nome}`);
|
||||||
|
return existing._id;
|
||||||
|
}
|
||||||
|
const id = await ctx.db.insert("roles", {
|
||||||
|
nome,
|
||||||
|
descricao,
|
||||||
|
nivel,
|
||||||
|
setor,
|
||||||
|
customizado: false,
|
||||||
|
editavel,
|
||||||
|
});
|
||||||
|
console.log(` ✅ Role criada: ${nome}`);
|
||||||
|
return id;
|
||||||
|
};
|
||||||
|
|
||||||
// ADMIN - Nível 2 - Permissões configuráveis
|
await ensureRole("ti_master", "TI Master", 0, "ti", false);
|
||||||
const roleAdmin = await ctx.db.insert("roles", {
|
await ensureRole("admin", "Administrador Geral", 2, "administrativo", true);
|
||||||
nome: "admin",
|
await ensureRole("ti_usuario", "TI Usuário", 2, "ti", true);
|
||||||
descricao: "Administrador Geral",
|
await ensureRole("rh", "Recursos Humanos", 2, "recursos_humanos", false);
|
||||||
nivel: 2,
|
await ensureRole("financeiro", "Financeiro", 2, "financeiro", false);
|
||||||
setor: "administrativo",
|
await ensureRole("usuario", "Usuário Padrão", 3, undefined, false);
|
||||||
customizado: false,
|
// Encadeia próxima etapa
|
||||||
editavel: true, // Permissões configuráveis
|
await ctx.scheduler.runAfter(0, internal.seed.seedCreateSimbolos, {});
|
||||||
});
|
return null;
|
||||||
console.log(" ✅ Role criada: admin (Nível 2 - Configurável)");
|
},
|
||||||
|
});
|
||||||
|
|
||||||
// TI_USUARIO - Nível 2 - Suporte técnico
|
export const seedCreateSimbolos = internalMutation({
|
||||||
const roleTIUsuario = await ctx.db.insert("roles", {
|
args: {},
|
||||||
nome: "ti_usuario",
|
returns: v.null(),
|
||||||
descricao: "TI Usuário",
|
handler: async (ctx) => {
|
||||||
nivel: 2,
|
|
||||||
setor: "ti",
|
|
||||||
customizado: false,
|
|
||||||
editavel: true,
|
|
||||||
});
|
|
||||||
console.log(" ✅ Role criada: ti_usuario (Nível 2 - Suporte)");
|
|
||||||
|
|
||||||
const roleRH = await ctx.db.insert("roles", {
|
|
||||||
nome: "rh",
|
|
||||||
descricao: "Recursos Humanos",
|
|
||||||
nivel: 2,
|
|
||||||
setor: "recursos_humanos",
|
|
||||||
customizado: false,
|
|
||||||
editavel: false,
|
|
||||||
});
|
|
||||||
console.log(" ✅ Role criada: rh");
|
|
||||||
|
|
||||||
const roleFinanceiro = await ctx.db.insert("roles", {
|
|
||||||
nome: "financeiro",
|
|
||||||
descricao: "Financeiro",
|
|
||||||
nivel: 2,
|
|
||||||
setor: "financeiro",
|
|
||||||
customizado: false,
|
|
||||||
editavel: false,
|
|
||||||
});
|
|
||||||
console.log(" ✅ Role criada: financeiro");
|
|
||||||
|
|
||||||
const roleUsuario = await ctx.db.insert("roles", {
|
|
||||||
nome: "usuario",
|
|
||||||
descricao: "Usuário Padrão",
|
|
||||||
nivel: 3,
|
|
||||||
setor: undefined,
|
|
||||||
customizado: false,
|
|
||||||
editavel: false,
|
|
||||||
});
|
|
||||||
console.log(" ✅ Role criada: usuario (Nível 3 - Padrão)");
|
|
||||||
|
|
||||||
// 2. Criar Símbolos (Cargos)
|
|
||||||
console.log("💰 Criando símbolos...");
|
console.log("💰 Criando símbolos...");
|
||||||
const simbolosMap = new Map<string, Id<"simbolos">>();
|
const existentes = await ctx.db.query("simbolos").collect();
|
||||||
|
const nomesExistentes = new Set(existentes.map((s) => s.nome));
|
||||||
for (const simbolo of simbolosData) {
|
for (const simbolo of simbolosData) {
|
||||||
const simboloId = await ctx.db.insert("simbolos", {
|
if (nomesExistentes.has(simbolo.nome)) {
|
||||||
|
console.log(` ℹ️ Símbolo já existe: ${simbolo.nome}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
await ctx.db.insert("simbolos", {
|
||||||
nome: simbolo.nome,
|
nome: simbolo.nome,
|
||||||
descricao: simbolo.descricao,
|
descricao: simbolo.descricao,
|
||||||
tipo: simbolo.tipo,
|
tipo: simbolo.tipo,
|
||||||
@@ -271,24 +258,43 @@ export const seedDatabase = internalMutation({
|
|||||||
repValor: simbolo.repValor || "",
|
repValor: simbolo.repValor || "",
|
||||||
vencValor: simbolo.vencValor || "",
|
vencValor: simbolo.vencValor || "",
|
||||||
});
|
});
|
||||||
simbolosMap.set(simbolo.nome, simboloId);
|
|
||||||
console.log(` ✅ Símbolo criado: ${simbolo.nome}`);
|
console.log(` ✅ Símbolo criado: ${simbolo.nome}`);
|
||||||
}
|
}
|
||||||
|
// Encadeia próxima etapa
|
||||||
|
await ctx.scheduler.runAfter(0, internal.seed.seedCreateFuncionarios, {});
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
// 3. Criar Funcionários
|
export const seedCreateFuncionarios = internalMutation({
|
||||||
|
args: {},
|
||||||
|
returns: v.null(),
|
||||||
|
handler: async (ctx) => {
|
||||||
console.log("👥 Criando funcionários...");
|
console.log("👥 Criando funcionários...");
|
||||||
const funcionariosMap = new Map<string, Id<"funcionarios">>();
|
const simbolos = await ctx.db.query("simbolos").collect();
|
||||||
|
const simbolosMap = new Map<string, Id<"simbolos">>();
|
||||||
|
for (const s of simbolos) {
|
||||||
|
simbolosMap.set(s.nome, s._id as Id<"simbolos">);
|
||||||
|
}
|
||||||
|
|
||||||
for (const funcionario of funcionariosData) {
|
for (const funcionario of funcionariosData) {
|
||||||
const simboloId = simbolosMap.get(funcionario.simboloNome);
|
// Evitar duplicar por CPF
|
||||||
if (!simboloId) {
|
const existente = await ctx.db
|
||||||
|
.query("funcionarios")
|
||||||
|
.withIndex("by_cpf", (q) => q.eq("cpf", funcionario.cpf))
|
||||||
|
.first();
|
||||||
|
if (existente) {
|
||||||
console.log(
|
console.log(
|
||||||
` ❌ Símbolo não encontrado: ${funcionario.simboloNome}`
|
` ℹ️ Funcionário já existe (CPF ${funcionario.cpf}): ${existente.nome}`
|
||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
const simboloId = simbolosMap.get(funcionario.simboloNome);
|
||||||
const funcId = await ctx.db.insert("funcionarios", {
|
if (!simboloId) {
|
||||||
|
console.log(` ❌ Símbolo não encontrado: ${funcionario.simboloNome}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
await ctx.db.insert("funcionarios", {
|
||||||
admissaoData: funcionario.admissaoData,
|
admissaoData: funcionario.admissaoData,
|
||||||
cep: funcionario.cep,
|
cep: funcionario.cep,
|
||||||
cidade: funcionario.cidade,
|
cidade: funcionario.cidade,
|
||||||
@@ -304,36 +310,126 @@ export const seedDatabase = internalMutation({
|
|||||||
telefone: funcionario.telefone,
|
telefone: funcionario.telefone,
|
||||||
uf: funcionario.uf,
|
uf: funcionario.uf,
|
||||||
});
|
});
|
||||||
funcionariosMap.set(funcionario.matricula, funcId);
|
|
||||||
console.log(` ✅ Funcionário criado: ${funcionario.nome}`);
|
console.log(` ✅ Funcionário criado: ${funcionario.nome}`);
|
||||||
}
|
}
|
||||||
|
// Encadeia próxima etapa
|
||||||
|
await ctx.scheduler.runAfter(
|
||||||
|
0,
|
||||||
|
internal.seed.seedCreateUsuariosParaFuncionarios,
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
// 5. Criar usuários para os funcionários
|
export const seedCreateUsuariosParaFuncionarios = internalMutation({
|
||||||
|
args: {},
|
||||||
|
returns: v.null(),
|
||||||
|
handler: async (ctx) => {
|
||||||
console.log("👤 Criando usuários para funcionários...");
|
console.log("👤 Criando usuários para funcionários...");
|
||||||
|
// Agenda criação por funcionário para evitar timeout
|
||||||
|
let delay = 0;
|
||||||
for (const funcionario of funcionariosData) {
|
for (const funcionario of funcionariosData) {
|
||||||
const funcId = funcionariosMap.get(funcionario.matricula);
|
await ctx.scheduler.runAfter(
|
||||||
if (!funcId) continue;
|
delay,
|
||||||
|
internal.seed.seedCreateUsuarioParaFuncionario,
|
||||||
const senhaInicial = await hashPassword("Mudar@123");
|
{
|
||||||
await ctx.db.insert("usuarios", {
|
matricula: funcionario.matricula,
|
||||||
senhaHash: senhaInicial,
|
nome: funcionario.nome,
|
||||||
nome: funcionario.nome,
|
email: funcionario.email,
|
||||||
email: funcionario.email,
|
}
|
||||||
funcionarioId: funcId as Id<"funcionarios">,
|
|
||||||
roleId: roleUsuario,
|
|
||||||
ativo: true,
|
|
||||||
primeiroAcesso: true,
|
|
||||||
criadoEm: Date.now(),
|
|
||||||
atualizadoEm: Date.now(),
|
|
||||||
});
|
|
||||||
console.log(
|
|
||||||
` ✅ Usuário criado: ${funcionario.nome} (senha: Mudar@123)`
|
|
||||||
);
|
);
|
||||||
|
delay += 50;
|
||||||
|
}
|
||||||
|
// Agenda próxima etapa após as criações individuais
|
||||||
|
await ctx.scheduler.runAfter(
|
||||||
|
delay + 300,
|
||||||
|
internal.seed.seedInserirSolicitacoesAcesso,
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const seedCreateUsuarioParaFuncionario = internalMutation({
|
||||||
|
args: {
|
||||||
|
matricula: v.string(),
|
||||||
|
nome: v.string(),
|
||||||
|
email: v.string(),
|
||||||
|
},
|
||||||
|
returns: v.null(),
|
||||||
|
handler: async (ctx, args) => {
|
||||||
|
// Role "usuario"
|
||||||
|
const roleUsuario = await ctx.db
|
||||||
|
.query("roles")
|
||||||
|
.withIndex("by_nome", (q) => q.eq("nome", "usuario"))
|
||||||
|
.first();
|
||||||
|
if (!roleUsuario) {
|
||||||
|
console.log(' ❌ Role "usuario" não encontrada');
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. Inserir solicitações de acesso
|
const funcionarioDoc = await ctx.db
|
||||||
|
.query("funcionarios")
|
||||||
|
.withIndex("by_matricula", (q) => q.eq("matricula", args.matricula))
|
||||||
|
.first();
|
||||||
|
if (!funcionarioDoc) {
|
||||||
|
console.log(
|
||||||
|
` ❌ Funcionário não encontrado pela matrícula: ${args.matricula}`
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const usuarioExistente = await ctx.db
|
||||||
|
.query("usuarios")
|
||||||
|
.withIndex("by_email", (q) => q.eq("email", args.email))
|
||||||
|
.first();
|
||||||
|
if (usuarioExistente) {
|
||||||
|
console.log(` ℹ️ Usuário já existe para ${args.email}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const authUserId = await createAuthUser(ctx, {
|
||||||
|
nome: args.nome,
|
||||||
|
email: args.email,
|
||||||
|
password: "Mudar@123",
|
||||||
|
});
|
||||||
|
|
||||||
|
await ctx.db.insert("usuarios", {
|
||||||
|
authId: authUserId,
|
||||||
|
nome: args.nome,
|
||||||
|
email: args.email,
|
||||||
|
funcionarioId: funcionarioDoc._id as Id<"funcionarios">,
|
||||||
|
roleId: roleUsuario._id,
|
||||||
|
ativo: true,
|
||||||
|
primeiroAcesso: true,
|
||||||
|
criadoEm: Date.now(),
|
||||||
|
atualizadoEm: Date.now(),
|
||||||
|
});
|
||||||
|
console.log(` ✅ Usuário criado: ${args.nome} (senha: Mudar@123)`);
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const seedInserirSolicitacoesAcesso = internalMutation({
|
||||||
|
args: {},
|
||||||
|
returns: v.null(),
|
||||||
|
handler: async (ctx) => {
|
||||||
console.log("📋 Inserindo solicitações de acesso...");
|
console.log("📋 Inserindo solicitações de acesso...");
|
||||||
for (const solicitacao of solicitacoesAcessoData) {
|
for (const solicitacao of solicitacoesAcessoData) {
|
||||||
|
// Evitar duplicidade por matrícula
|
||||||
|
const existente = await ctx.db
|
||||||
|
.query("solicitacoesAcesso")
|
||||||
|
.withIndex("by_matricula", (q) =>
|
||||||
|
q.eq("matricula", solicitacao.matricula)
|
||||||
|
)
|
||||||
|
.first();
|
||||||
|
if (existente) {
|
||||||
|
console.log(
|
||||||
|
` ℹ️ Solicitação já existe p/ matrícula ${solicitacao.matricula}`
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const dadosSolicitacao: {
|
const dadosSolicitacao: {
|
||||||
nome: string;
|
nome: string;
|
||||||
matricula: string;
|
matricula: string;
|
||||||
@@ -365,10 +461,24 @@ export const seedDatabase = internalMutation({
|
|||||||
` ✅ Solicitação criada: ${solicitacao.nome} (${solicitacao.status})`
|
` ✅ Solicitação criada: ${solicitacao.nome} (${solicitacao.status})`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
console.log("✨ Seed concluído!");
|
||||||
console.log("✨ Seed do banco de dados concluído com sucesso!");
|
|
||||||
return null;
|
return null;
|
||||||
}
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const seedDatabase = internalAction({
|
||||||
|
args: {},
|
||||||
|
returns: v.null(),
|
||||||
|
handler: async (ctx) => {
|
||||||
|
console.log("🌱 Iniciando seed do banco de dados (action)...");
|
||||||
|
await ctx.runMutation(internal.seed.seedCreateRoles, {});
|
||||||
|
await ctx.runMutation(internal.seed.seedCreateSimbolos, {});
|
||||||
|
await ctx.runMutation(internal.seed.seedCreateFuncionarios, {});
|
||||||
|
await ctx.runMutation(internal.seed.seedCreateUsuariosParaFuncionarios, {});
|
||||||
|
await ctx.runMutation(internal.seed.seedInserirSolicitacoesAcesso, {});
|
||||||
|
console.log("✨ Seed do banco de dados concluído com sucesso pela action!");
|
||||||
|
return null;
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -379,10 +489,12 @@ export const popularBanco = mutation({
|
|||||||
args: {},
|
args: {},
|
||||||
returns: v.null(),
|
returns: v.null(),
|
||||||
handler: async (ctx) => {
|
handler: async (ctx) => {
|
||||||
console.log("🌱 Executando popularBanco (wrapper público para seedDatabase)...");
|
console.log(
|
||||||
// Chama a internalMutation para reaproveitar a lógica de seed
|
"🌱 Executando popularBanco (wrapper público para seedDatabase)..."
|
||||||
await ctx.runMutation(internal.seed.seedDatabase, {});
|
);
|
||||||
console.log("✅ Seed concluído pelo wrapper público");
|
// Agenda apenas a primeira etapa; as demais serão encadeadas internamente
|
||||||
|
await ctx.scheduler.runAfter(0, internal.seed.seedCreateRoles, {});
|
||||||
|
console.log("✅ Seed iniciado (etapa 1 agendada)");
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -948,7 +1060,8 @@ export const verificarBanco = query({
|
|||||||
funcionarios: funcionarios.length,
|
funcionarios: funcionarios.length,
|
||||||
roles: roles.length,
|
roles: roles.length,
|
||||||
simbolos: simbolos.length,
|
simbolos: simbolos.length,
|
||||||
total: usuarios.length + funcionarios.length + roles.length + simbolos.length,
|
total:
|
||||||
|
usuarios.length + funcionarios.length + roles.length + simbolos.length,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { v } from "convex/values";
|
import { v } from "convex/values";
|
||||||
import { mutation, query } from "./_generated/server";
|
import { mutation, query } from "./_generated/server";
|
||||||
import { hashPassword, generateToken } from "./auth/utils";
|
import { hashPassword } from "./auth/utils";
|
||||||
import { registrarAtividade } from "./logsAtividades";
|
import { registrarAtividade } from "./logsAtividades";
|
||||||
import { Id, Doc } from "./_generated/dataModel";
|
import { Id, Doc } from "./_generated/dataModel";
|
||||||
import { api } from "./_generated/api";
|
|
||||||
import type { QueryCtx, MutationCtx } from "./_generated/server";
|
import type { QueryCtx, MutationCtx } from "./_generated/server";
|
||||||
|
import { createAuthUser, getCurrentUserFunction } from "./auth";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper para obter a matrícula do usuário (do funcionário se houver)
|
* Helper para obter a matrícula do usuário (do funcionário se houver)
|
||||||
@@ -20,33 +20,6 @@ async function obterMatriculaUsuario(
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper para obter usuário autenticado (Better Auth ou Sessão)
|
|
||||||
* Usa a mesma lógica do obterPerfil para garantir consistência
|
|
||||||
*/
|
|
||||||
async function getUsuarioAutenticado(ctx: QueryCtx | MutationCtx) {
|
|
||||||
// FASE 1 IMPLEMENTADA: Usa Custom Auth Provider configurado no convex.config.ts
|
|
||||||
// O provider busca sessão por token específico (seguro) ou Better Auth
|
|
||||||
const identity = await ctx.auth.getUserIdentity();
|
|
||||||
let usuarioAtual = null;
|
|
||||||
|
|
||||||
if (identity && identity.email) {
|
|
||||||
usuarioAtual = await ctx.db
|
|
||||||
.query("usuarios")
|
|
||||||
.withIndex("by_email", (q) => q.eq("email", identity.email!))
|
|
||||||
.first();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!usuarioAtual && identity) {
|
|
||||||
console.error("⚠️ [getUsuarioAutenticado] Identity encontrada mas usuário não encontrado no banco:", {
|
|
||||||
email: identity.email,
|
|
||||||
subject: identity.subject
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return usuarioAtual;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Associar funcionário a um usuário
|
* Associar funcionário a um usuário
|
||||||
*/
|
*/
|
||||||
@@ -132,12 +105,17 @@ export const criar = mutation({
|
|||||||
return { sucesso: false as const, erro: "E-mail já cadastrado" };
|
return { sucesso: false as const, erro: "E-mail já cadastrado" };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Gerar hash da senha inicial
|
const senhaTemporaria = args.senhaInicial;
|
||||||
const senhaHash = await hashPassword(args.senhaInicial);
|
|
||||||
|
const authUserId = await createAuthUser(ctx, {
|
||||||
|
nome: args.nome,
|
||||||
|
email: args.email,
|
||||||
|
password: senhaTemporaria,
|
||||||
|
});
|
||||||
|
|
||||||
// Criar usuário
|
// Criar usuário
|
||||||
const usuarioId = await ctx.db.insert("usuarios", {
|
const usuarioId = await ctx.db.insert("usuarios", {
|
||||||
senhaHash,
|
authId: authUserId,
|
||||||
nome: args.nome,
|
nome: args.nome,
|
||||||
email: args.email,
|
email: args.email,
|
||||||
funcionarioId: args.funcionarioId,
|
funcionarioId: args.funcionarioId,
|
||||||
@@ -161,69 +139,6 @@ export const listar = query({
|
|||||||
matricula: v.optional(v.string()),
|
matricula: v.optional(v.string()),
|
||||||
ativo: v.optional(v.boolean()),
|
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.union(
|
|
||||||
// v.object({
|
|
||||||
// _id: v.id("roles"),
|
|
||||||
// _creationTime: v.optional(v.number()),
|
|
||||||
// criadoPor: v.optional(v.id("usuarios")),
|
|
||||||
// customizado: v.optional(v.boolean()),
|
|
||||||
// descricao: v.string(),
|
|
||||||
// editavel: v.optional(v.boolean()),
|
|
||||||
// nome: v.string(),
|
|
||||||
// nivel: v.number(),
|
|
||||||
// setor: v.optional(v.string()),
|
|
||||||
// }),
|
|
||||||
// v.object({
|
|
||||||
// _id: v.id("roles"),
|
|
||||||
// _creationTime: v.optional(v.number()),
|
|
||||||
// criadoPor: v.optional(v.id("usuarios")),
|
|
||||||
// customizado: v.optional(v.boolean()),
|
|
||||||
// descricao: v.literal("Perfil não encontrado"),
|
|
||||||
// editavel: v.optional(v.boolean()),
|
|
||||||
// nome: v.literal("erro_role_ausente"),
|
|
||||||
// nivel: v.literal(999),
|
|
||||||
// setor: v.optional(v.string()),
|
|
||||||
// erro: v.literal(true),
|
|
||||||
// })
|
|
||||||
// ),
|
|
||||||
// funcionario: v.optional(
|
|
||||||
// v.object({
|
|
||||||
// _id: v.id("funcionarios"),
|
|
||||||
// nome: v.string(),
|
|
||||||
// matricula: v.optional(v.string()),
|
|
||||||
// descricaoCargo: v.optional(v.string()),
|
|
||||||
// simboloTipo: v.union(
|
|
||||||
// v.literal("cargo_comissionado"),
|
|
||||||
// v.literal("funcao_gratificada")
|
|
||||||
// ),
|
|
||||||
// })
|
|
||||||
// ),
|
|
||||||
// avisos: v.optional(
|
|
||||||
// v.array(
|
|
||||||
// v.object({
|
|
||||||
// tipo: v.union(
|
|
||||||
// v.literal("erro"),
|
|
||||||
// v.literal("aviso"),
|
|
||||||
// v.literal("info")
|
|
||||||
// ),
|
|
||||||
// mensagem: v.string(),
|
|
||||||
// })
|
|
||||||
// )
|
|
||||||
// ),
|
|
||||||
// })
|
|
||||||
// ),
|
|
||||||
handler: async (ctx, args) => {
|
handler: async (ctx, args) => {
|
||||||
let usuarios = await ctx.db.query("usuarios").collect();
|
let usuarios = await ctx.db.query("usuarios").collect();
|
||||||
|
|
||||||
@@ -439,34 +354,34 @@ export const alterarStatus = mutation({
|
|||||||
/**
|
/**
|
||||||
* Resetar senha do usuário
|
* Resetar senha do usuário
|
||||||
*/
|
*/
|
||||||
export const resetarSenha = mutation({
|
// export const resetarSenha = mutation({
|
||||||
args: {
|
// args: {
|
||||||
usuarioId: v.id("usuarios"),
|
// usuarioId: v.id("usuarios"),
|
||||||
novaSenha: v.string(),
|
// novaSenha: v.string(),
|
||||||
},
|
// },
|
||||||
returns: v.null(),
|
// returns: v.null(),
|
||||||
handler: async (ctx, args) => {
|
// handler: async (ctx, args) => {
|
||||||
const senhaHash = await hashPassword(args.novaSenha);
|
// const senhaHash = await hashPassword(args.novaSenha);
|
||||||
|
|
||||||
await ctx.db.patch(args.usuarioId, {
|
// await ctx.db.patch(args.usuarioId, {
|
||||||
senhaHash,
|
// senhaHash,
|
||||||
primeiroAcesso: true,
|
// primeiroAcesso: true,
|
||||||
atualizadoEm: Date.now(),
|
// atualizadoEm: Date.now(),
|
||||||
});
|
// });
|
||||||
|
|
||||||
// Desativar todas as sessões
|
// // Desativar todas as sessões
|
||||||
const sessoes = await ctx.db
|
// const sessoes = await ctx.db
|
||||||
.query("sessoes")
|
// .query("sessoes")
|
||||||
.withIndex("by_usuario", (q) => q.eq("usuarioId", args.usuarioId))
|
// .withIndex("by_usuario", (q) => q.eq("usuarioId", args.usuarioId))
|
||||||
.collect();
|
// .collect();
|
||||||
|
|
||||||
for (const sessao of sessoes) {
|
// for (const sessao of sessoes) {
|
||||||
await ctx.db.patch(sessao._id, { ativo: false });
|
// await ctx.db.patch(sessao._id, { ativo: false });
|
||||||
}
|
// }
|
||||||
|
|
||||||
return null;
|
// return null;
|
||||||
},
|
// },
|
||||||
});
|
// });
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Excluir usuário
|
* Excluir usuário
|
||||||
@@ -601,19 +516,6 @@ export const atualizarPerfil = mutation({
|
|||||||
.first();
|
.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");
|
if (!usuarioAtual) throw new Error("Usuário não encontrado");
|
||||||
|
|
||||||
// Validar statusMensagem (max 100 chars)
|
// Validar statusMensagem (max 100 chars)
|
||||||
@@ -678,9 +580,6 @@ export const obterPerfil = query({
|
|||||||
v.null()
|
v.null()
|
||||||
),
|
),
|
||||||
handler: async (ctx) => {
|
handler: async (ctx) => {
|
||||||
console.log("=== DEBUG obterPerfil ===");
|
|
||||||
|
|
||||||
// TENTAR BETTER AUTH PRIMEIRO
|
|
||||||
const identity = await ctx.auth.getUserIdentity();
|
const identity = await ctx.auth.getUserIdentity();
|
||||||
console.log("Identity:", identity ? "encontrado" : "null");
|
console.log("Identity:", identity ? "encontrado" : "null");
|
||||||
|
|
||||||
@@ -693,42 +592,9 @@ export const obterPerfil = query({
|
|||||||
.query("usuarios")
|
.query("usuarios")
|
||||||
.withIndex("by_email", (q) => q.eq("email", identity.email!))
|
.withIndex("by_email", (q) => q.eq("email", identity.email!))
|
||||||
.first();
|
.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) {
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -789,7 +655,10 @@ export const listarParaChat = query({
|
|||||||
),
|
),
|
||||||
handler: async (ctx) => {
|
handler: async (ctx) => {
|
||||||
// Obter usuário autenticado usando função helper compartilhada
|
// Obter usuário autenticado usando função helper compartilhada
|
||||||
const usuarioAtual = await getUsuarioAutenticado(ctx);
|
const usuarioAtual = await getCurrentUserFunction(ctx);
|
||||||
|
if (!usuarioAtual) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
// Buscar todos os usuários ativos
|
// Buscar todos os usuários ativos
|
||||||
const usuarios = await ctx.db
|
const usuarios = await ctx.db
|
||||||
@@ -888,6 +757,11 @@ export const bloquearUsuario = mutation({
|
|||||||
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 usuarioAtual = await getCurrentUserFunction(ctx);
|
||||||
|
if (!usuarioAtual) {
|
||||||
|
return { sucesso: false as const, erro: "Usuário não autenticado" };
|
||||||
|
}
|
||||||
|
|
||||||
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" };
|
||||||
@@ -948,6 +822,11 @@ export const desbloquearUsuario = mutation({
|
|||||||
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 usuarioAtual = await getCurrentUserFunction(ctx);
|
||||||
|
if (!usuarioAtual) {
|
||||||
|
return { sucesso: false as const, erro: "Usuário não autenticado" };
|
||||||
|
}
|
||||||
|
|
||||||
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" };
|
||||||
@@ -995,48 +874,48 @@ 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
|
// // Gerar senha temporária se não foi fornecida
|
||||||
const senhaTemporaria = args.novaSenhaTemporaria || gerarSenhaTemporaria();
|
// const senhaTemporaria = args.novaSenhaTemporaria || gerarSenhaTemporaria();
|
||||||
const senhaHash = await hashPassword(senhaTemporaria);
|
// const senhaHash = await hashPassword(senhaTemporaria);
|
||||||
|
|
||||||
// Atualizar usuário
|
// // Atualizar usuário
|
||||||
await ctx.db.patch(args.usuarioId, {
|
// await ctx.db.patch(args.usuarioId, {
|
||||||
senhaHash,
|
// senhaHash,
|
||||||
primeiroAcesso: true, // Força mudança de senha no próximo login
|
// primeiroAcesso: true, // Força mudança de senha no próximo login
|
||||||
tentativasLogin: 0,
|
// tentativasLogin: 0,
|
||||||
ultimaTentativaLogin: undefined,
|
// ultimaTentativaLogin: undefined,
|
||||||
atualizadoEm: Date.now(),
|
// atualizadoEm: Date.now(),
|
||||||
});
|
// });
|
||||||
|
|
||||||
// Log de atividade
|
// // Log de atividade
|
||||||
await registrarAtividade(
|
// await registrarAtividade(
|
||||||
ctx,
|
// ctx,
|
||||||
args.resetadoPorId,
|
// args.resetadoPorId,
|
||||||
"resetar_senha",
|
// "resetar_senha",
|
||||||
"usuarios",
|
// "usuarios",
|
||||||
JSON.stringify({ usuarioId: args.usuarioId }),
|
// JSON.stringify({ usuarioId: args.usuarioId }),
|
||||||
args.usuarioId
|
// args.usuarioId
|
||||||
);
|
// );
|
||||||
|
|
||||||
return { sucesso: true as const, senhaTemporaria };
|
// return { sucesso: true as const, senhaTemporaria };
|
||||||
},
|
// },
|
||||||
});
|
// });
|
||||||
|
|
||||||
// Helper para gerar senha temporária
|
// Helper para gerar senha temporária
|
||||||
function gerarSenhaTemporaria(): string {
|
function gerarSenhaTemporaria(): string {
|
||||||
@@ -1066,6 +945,11 @@ export const editarUsuario = mutation({
|
|||||||
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 usuarioAtual = await getCurrentUserFunction(ctx);
|
||||||
|
if (!usuarioAtual) {
|
||||||
|
return { sucesso: false as const, erro: "Usuário não autenticado" };
|
||||||
|
}
|
||||||
|
|
||||||
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" };
|
||||||
@@ -1153,7 +1037,12 @@ export const criarAdminMaster = mutation({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const senhaTemporaria = args.senha || gerarSenhaTemporaria();
|
const senhaTemporaria = args.senha || gerarSenhaTemporaria();
|
||||||
const senhaHash = await hashPassword(senhaTemporaria);
|
|
||||||
|
const authUserId = await createAuthUser(ctx, {
|
||||||
|
nome: args.nome,
|
||||||
|
email: args.email,
|
||||||
|
password: senhaTemporaria,
|
||||||
|
});
|
||||||
|
|
||||||
// Verificar se email já existe
|
// Verificar se email já existe
|
||||||
const existentePorEmail = await ctx.db
|
const existentePorEmail = await ctx.db
|
||||||
@@ -1164,11 +1053,11 @@ export const criarAdminMaster = mutation({
|
|||||||
// Promove usuário existente por email
|
// Promove usuário existente por email
|
||||||
await ctx.db.patch(existentePorEmail._id, {
|
await ctx.db.patch(existentePorEmail._id, {
|
||||||
nome: args.nome,
|
nome: args.nome,
|
||||||
senhaHash,
|
|
||||||
roleId: roleTIMaster._id,
|
roleId: roleTIMaster._id,
|
||||||
ativo: true,
|
ativo: true,
|
||||||
primeiroAcesso: true,
|
primeiroAcesso: true,
|
||||||
atualizadoEm: Date.now(),
|
atualizadoEm: Date.now(),
|
||||||
|
authId: authUserId,
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
sucesso: true as const,
|
sucesso: true as const,
|
||||||
@@ -1179,7 +1068,7 @@ export const criarAdminMaster = mutation({
|
|||||||
|
|
||||||
// Criar novo usuário TI Master
|
// Criar novo usuário TI Master
|
||||||
const usuarioId = await ctx.db.insert("usuarios", {
|
const usuarioId = await ctx.db.insert("usuarios", {
|
||||||
senhaHash,
|
authId: authUserId,
|
||||||
nome: args.nome,
|
nome: args.nome,
|
||||||
email: args.email,
|
email: args.email,
|
||||||
roleId: roleTIMaster._id,
|
roleId: roleTIMaster._id,
|
||||||
@@ -1192,191 +1081,3 @@ export const criarAdminMaster = mutation({
|
|||||||
return { sucesso: true as const, usuarioId, senhaTemporaria };
|
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: {
|
|
||||||
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 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", {
|
|
||||||
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, 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: {
|
|
||||||
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 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 email
|
|
||||||
const existentePorEmail = await ctx.db
|
|
||||||
.query("usuarios")
|
|
||||||
.withIndex("by_email", (q) => q.eq("email", email))
|
|
||||||
.first();
|
|
||||||
|
|
||||||
const senhaHash = await hashPassword(senha);
|
|
||||||
|
|
||||||
if (existentePorEmail) {
|
|
||||||
await ctx.db.patch(existentePorEmail._id, {
|
|
||||||
nome,
|
|
||||||
email,
|
|
||||||
senhaHash,
|
|
||||||
roleId: roleAdmin._id,
|
|
||||||
ativo: true,
|
|
||||||
primeiroAcesso: false,
|
|
||||||
atualizadoEm: Date.now(),
|
|
||||||
});
|
|
||||||
return { sucesso: true, usuarioId: existentePorEmail._id };
|
|
||||||
}
|
|
||||||
|
|
||||||
const usuarioId = await ctx.db.insert("usuarios", {
|
|
||||||
senhaHash,
|
|
||||||
nome,
|
|
||||||
email,
|
|
||||||
roleId: roleAdmin._id,
|
|
||||||
ativo: true,
|
|
||||||
primeiroAcesso: false,
|
|
||||||
criadoEm: Date.now(),
|
|
||||||
atualizadoEm: Date.now(),
|
|
||||||
});
|
|
||||||
|
|
||||||
return { sucesso: true, usuarioId };
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|||||||
Reference in New Issue
Block a user