feat: enhance employee and symbol management with new features, improved UI components, and backend schema updates
This commit is contained in:
381
packages/backend/convex/autenticacao.ts
Normal file
381
packages/backend/convex/autenticacao.ts
Normal file
@@ -0,0 +1,381 @@
|
||||
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 };
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user