- Implemented an internal mutation for login that captures the user's IP address and user agent for better security and tracking. - Enhanced the HTTP login endpoint to extract and log client IP, improving the overall authentication process. - Added validation for IP addresses to ensure only valid formats are recorded, enhancing data integrity. - Updated the login mutation to handle rate limiting and user status checks more effectively, providing clearer feedback on login attempts.
814 lines
22 KiB
TypeScript
814 lines
22 KiB
TypeScript
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,
|
|
});
|
|
|
|
return {
|
|
sucesso: true as const,
|
|
token,
|
|
usuario: {
|
|
_id: usuario._id,
|
|
matricula: usuario.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,
|
|
});
|
|
|
|
return {
|
|
sucesso: true as const,
|
|
token,
|
|
usuario: {
|
|
_id: usuario._id,
|
|
matricula: usuario.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" };
|
|
}
|
|
|
|
return {
|
|
valido: true as const,
|
|
usuario: {
|
|
_id: usuario._id,
|
|
matricula: usuario.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 };
|
|
},
|
|
});
|