refactor: remove authentication module and integrate Better Auth
- Deleted the `autenticacao.ts` file to streamline the authentication process. - Updated the `auth.ts` file to include new functions for user management and password updates. - Modified the schema to enforce the presence of `authId` for users, ensuring integration with Better Auth. - Refactored the seed process to create users with Better Auth integration, enhancing user management capabilities. - Cleaned up the `usuarios.ts` file to utilize the new authentication functions and improve code clarity.
This commit is contained in:
@@ -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 };
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user