382 lines
9.2 KiB
TypeScript
382 lines
9.2 KiB
TypeScript
import { v } from "convex/values";
|
|
import { mutation, query } from "./_generated/server";
|
|
import { hashPassword, verifyPassword, generateToken, validarMatricula, validarSenha } from "./auth/utils";
|
|
|
|
/**
|
|
* Login do usuário
|
|
*/
|
|
export const login = mutation({
|
|
args: {
|
|
matricula: 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(),
|
|
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) => {
|
|
// Validar matrícula
|
|
if (!validarMatricula(args.matricula)) {
|
|
return {
|
|
sucesso: false as const,
|
|
erro: "Matrícula inválida. Use apenas números.",
|
|
};
|
|
}
|
|
|
|
// Buscar usuário
|
|
const usuario = await ctx.db
|
|
.query("usuarios")
|
|
.withIndex("by_matricula", (q) => q.eq("matricula", args.matricula))
|
|
.first();
|
|
|
|
if (!usuario) {
|
|
// Log de tentativa de acesso negado
|
|
await ctx.db.insert("logsAcesso", {
|
|
usuarioId: "" as any, // Não temos ID
|
|
tipo: "acesso_negado",
|
|
ipAddress: args.ipAddress,
|
|
userAgent: args.userAgent,
|
|
detalhes: `Tentativa de login com matrícula inexistente: ${args.matricula}`,
|
|
timestamp: Date.now(),
|
|
});
|
|
|
|
return {
|
|
sucesso: false as const,
|
|
erro: "Matrícula ou senha incorreta.",
|
|
};
|
|
}
|
|
|
|
// Verificar se usuário está ativo
|
|
if (!usuario.ativo) {
|
|
await ctx.db.insert("logsAcesso", {
|
|
usuarioId: usuario._id,
|
|
tipo: "acesso_negado",
|
|
ipAddress: args.ipAddress,
|
|
userAgent: args.userAgent,
|
|
detalhes: "Tentativa de login com usuário inativo",
|
|
timestamp: Date.now(),
|
|
});
|
|
|
|
return {
|
|
sucesso: false as const,
|
|
erro: "Usuário inativo. Entre em contato com o TI.",
|
|
};
|
|
}
|
|
|
|
// Verificar senha
|
|
const senhaValida = await verifyPassword(args.senha, usuario.senhaHash);
|
|
|
|
if (!senhaValida) {
|
|
await ctx.db.insert("logsAcesso", {
|
|
usuarioId: usuario._id,
|
|
tipo: "acesso_negado",
|
|
ipAddress: args.ipAddress,
|
|
userAgent: args.userAgent,
|
|
detalhes: "Senha incorreta",
|
|
timestamp: Date.now(),
|
|
});
|
|
|
|
return {
|
|
sucesso: false as const,
|
|
erro: "Matrícula ou senha incorreta.",
|
|
};
|
|
}
|
|
|
|
// Buscar role do usuário
|
|
const role = 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 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,
|
|
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(),
|
|
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 = 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 = 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 = 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,
|
|
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 = 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 = 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 };
|
|
},
|
|
});
|
|
|