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 }; }, });