feat: enhance login process with IP capture and improved error handling
- 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.
This commit is contained in:
4
packages/backend/convex/_generated/api.d.ts
vendored
4
packages/backend/convex/_generated/api.d.ts
vendored
@@ -10,6 +10,7 @@
|
||||
|
||||
import type * as actions_email from "../actions/email.js";
|
||||
import type * as actions_smtp from "../actions/smtp.js";
|
||||
import type * as atestadosLicencas from "../atestadosLicencas.js";
|
||||
import type * as autenticacao from "../autenticacao.js";
|
||||
import type * as auth_utils from "../auth/utils.js";
|
||||
import type * as chat from "../chat.js";
|
||||
@@ -38,6 +39,7 @@ import type * as templatesMensagens from "../templatesMensagens.js";
|
||||
import type * as times from "../times.js";
|
||||
import type * as todos from "../todos.js";
|
||||
import type * as usuarios from "../usuarios.js";
|
||||
import type * as utils_getClientIP from "../utils/getClientIP.js";
|
||||
import type * as verificarMatriculas from "../verificarMatriculas.js";
|
||||
|
||||
import type {
|
||||
@@ -57,6 +59,7 @@ import type {
|
||||
declare const fullApi: ApiFromModules<{
|
||||
"actions/email": typeof actions_email;
|
||||
"actions/smtp": typeof actions_smtp;
|
||||
atestadosLicencas: typeof atestadosLicencas;
|
||||
autenticacao: typeof autenticacao;
|
||||
"auth/utils": typeof auth_utils;
|
||||
chat: typeof chat;
|
||||
@@ -85,6 +88,7 @@ declare const fullApi: ApiFromModules<{
|
||||
times: typeof times;
|
||||
todos: typeof todos;
|
||||
usuarios: typeof usuarios;
|
||||
"utils/getClientIP": typeof utils_getClientIP;
|
||||
verificarMatriculas: typeof verificarMatriculas;
|
||||
}>;
|
||||
declare const fullApiWithMounts: typeof fullApi;
|
||||
|
||||
1048
packages/backend/convex/atestadosLicencas.ts
Normal file
1048
packages/backend/convex/atestadosLicencas.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
||||
import { v } from "convex/values";
|
||||
import { mutation, query } from "./_generated/server";
|
||||
import { mutation, query, internalMutation } from "./_generated/server";
|
||||
import {
|
||||
hashPassword,
|
||||
verifyPassword,
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
} from "./auth/utils";
|
||||
import { registrarLogin } from "./logsLogin";
|
||||
import { Id, Doc } from "./_generated/dataModel";
|
||||
import type { QueryCtx } from "./_generated/server";
|
||||
import type { QueryCtx, MutationCtx } from "./_generated/server";
|
||||
|
||||
/**
|
||||
* Helper para verificar se usuário está bloqueado
|
||||
@@ -315,6 +315,280 @@ export const login = mutation({
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
@@ -1,5 +1,150 @@
|
||||
import { httpRouter } from "convex/server";
|
||||
|
||||
const http = httpRouter();
|
||||
|
||||
export default http;
|
||||
import { httpRouter } from "convex/server";
|
||||
import { httpAction } from "./_generated/server";
|
||||
import { internal } from "./_generated/api";
|
||||
import { getClientIP } from "./utils/getClientIP";
|
||||
import { v } from "convex/values";
|
||||
|
||||
const http = httpRouter();
|
||||
|
||||
/**
|
||||
* Endpoint de teste para debug - retorna todos os headers disponíveis
|
||||
* GET /api/debug/headers
|
||||
*/
|
||||
http.route({
|
||||
path: "/api/debug/headers",
|
||||
method: "GET",
|
||||
handler: httpAction(async (ctx, request) => {
|
||||
const headers: Record<string, string> = {};
|
||||
request.headers.forEach((value, key) => {
|
||||
headers[key] = value;
|
||||
});
|
||||
|
||||
const ip = getClientIP(request);
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
headers,
|
||||
extractedIP: ip,
|
||||
url: request.url,
|
||||
}),
|
||||
{
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}
|
||||
);
|
||||
}),
|
||||
});
|
||||
|
||||
/**
|
||||
* Endpoint HTTP para login que captura automaticamente o IP do cliente
|
||||
* POST /api/login
|
||||
* Body: { matriculaOuEmail: string, senha: string }
|
||||
*/
|
||||
http.route({
|
||||
path: "/api/login",
|
||||
method: "POST",
|
||||
handler: httpAction(async (ctx, request) => {
|
||||
try {
|
||||
// Debug: Log todos os headers disponíveis
|
||||
console.log("=== DEBUG: Headers HTTP ===");
|
||||
const headersEntries: string[] = [];
|
||||
request.headers.forEach((value, key) => {
|
||||
headersEntries.push(`${key}: ${value}`);
|
||||
});
|
||||
console.log("Headers:", headersEntries.join(", "));
|
||||
console.log("Request URL:", request.url);
|
||||
|
||||
// Extrair IP do cliente do request
|
||||
let clientIP = getClientIP(request);
|
||||
console.log("IP extraído:", clientIP);
|
||||
|
||||
// Se não encontrou IP, tentar obter do URL ou usar valor padrão
|
||||
if (!clientIP) {
|
||||
try {
|
||||
const url = new URL(request.url);
|
||||
// Tentar pegar do query param se disponível
|
||||
const ipParam = url.searchParams.get("client_ip");
|
||||
if (ipParam && /^(\d{1,3}\.){3}\d{1,3}$/.test(ipParam)) {
|
||||
clientIP = ipParam;
|
||||
console.log("IP obtido do query param:", clientIP);
|
||||
} else {
|
||||
// Se ainda não tiver IP, usar um identificador baseado no timestamp
|
||||
// Isso pelo menos diferencia requisições
|
||||
console.warn("IP não encontrado nos headers. Usando fallback.");
|
||||
clientIP = undefined; // Deixar como undefined para registrar como não disponível
|
||||
}
|
||||
} catch {
|
||||
console.warn("Erro ao processar URL para IP");
|
||||
}
|
||||
}
|
||||
|
||||
// Extrair User-Agent
|
||||
const userAgent = request.headers.get("user-agent") || undefined;
|
||||
|
||||
// Ler body da requisição
|
||||
const body = await request.json();
|
||||
|
||||
if (!body.matriculaOuEmail || !body.senha) {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
sucesso: false,
|
||||
erro: "Matrícula/Email e senha são obrigatórios",
|
||||
}),
|
||||
{
|
||||
status: 400,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Chamar a mutation de login interna com IP e userAgent
|
||||
const resultado = await ctx.runMutation(internal.autenticacao.loginComIP, {
|
||||
matriculaOuEmail: body.matriculaOuEmail,
|
||||
senha: body.senha,
|
||||
ipAddress: clientIP,
|
||||
userAgent: userAgent,
|
||||
});
|
||||
|
||||
return new Response(JSON.stringify(resultado), {
|
||||
status: 200,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"Access-Control-Allow-Methods": "POST, OPTIONS",
|
||||
"Access-Control-Allow-Headers": "Content-Type",
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
sucesso: false,
|
||||
erro: error instanceof Error ? error.message : "Erro ao processar login",
|
||||
}),
|
||||
{
|
||||
status: 500,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}
|
||||
);
|
||||
}
|
||||
}),
|
||||
});
|
||||
|
||||
/**
|
||||
* Endpoint OPTIONS para CORS preflight
|
||||
*/
|
||||
http.route({
|
||||
path: "/api/login",
|
||||
method: "OPTIONS",
|
||||
handler: httpAction(async () => {
|
||||
return new Response(null, {
|
||||
status: 200,
|
||||
headers: {
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"Access-Control-Allow-Methods": "POST, OPTIONS",
|
||||
"Access-Control-Allow-Headers": "Content-Type",
|
||||
},
|
||||
});
|
||||
}),
|
||||
});
|
||||
|
||||
export default http;
|
||||
|
||||
@@ -5,6 +5,35 @@ import { Doc, Id } from "./_generated/dataModel";
|
||||
/**
|
||||
* Helper para registrar tentativas de login
|
||||
*/
|
||||
/**
|
||||
* Valida se uma string é um IP válido
|
||||
*/
|
||||
function validarIP(ip: string | undefined): string | undefined {
|
||||
if (!ip || ip.length < 7) return undefined; // IP mínimo: "1.1.1.1" = 7 chars
|
||||
|
||||
// Validar IPv4
|
||||
const ipv4Regex = /^(\d{1,3}\.){3}\d{1,3}$/;
|
||||
if (ipv4Regex.test(ip)) {
|
||||
const parts = ip.split('.');
|
||||
if (parts.length === 4 && parts.every(part => {
|
||||
const num = parseInt(part, 10);
|
||||
return !isNaN(num) && num >= 0 && num <= 255;
|
||||
})) {
|
||||
return ip;
|
||||
}
|
||||
}
|
||||
|
||||
// Validar IPv6 básico
|
||||
const ipv6Regex = /^([0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}$|^::[0-9a-fA-F]{0,4}(:[0-9a-fA-F]{0,4}){0,6}$|^[0-9a-fA-F]{0,4}::[0-9a-fA-F]{0,4}(:[0-9a-fA-F]{0,4}){0,5}$/;
|
||||
if (ipv6Regex.test(ip)) {
|
||||
return ip;
|
||||
}
|
||||
|
||||
// IP inválido - não salvar
|
||||
console.warn(`IP inválido detectado e ignorado: "${ip}"`);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export async function registrarLogin(
|
||||
ctx: MutationCtx,
|
||||
dados: {
|
||||
@@ -21,12 +50,15 @@ export async function registrarLogin(
|
||||
const browser = dados.userAgent ? extrairBrowser(dados.userAgent) : undefined;
|
||||
const sistema = dados.userAgent ? extrairSistema(dados.userAgent) : undefined;
|
||||
|
||||
// Validar e sanitizar IP antes de salvar
|
||||
const ipAddressValidado = validarIP(dados.ipAddress);
|
||||
|
||||
await ctx.db.insert("logsLogin", {
|
||||
usuarioId: dados.usuarioId,
|
||||
matriculaOuEmail: dados.matriculaOuEmail,
|
||||
sucesso: dados.sucesso,
|
||||
motivoFalha: dados.motivoFalha,
|
||||
ipAddress: dados.ipAddress,
|
||||
ipAddress: ipAddressValidado,
|
||||
userAgent: dados.userAgent,
|
||||
device,
|
||||
browser,
|
||||
|
||||
@@ -151,11 +151,43 @@ export default defineSchema({
|
||||
|
||||
atestados: defineTable({
|
||||
funcionarioId: v.id("funcionarios"),
|
||||
tipo: v.union(
|
||||
v.literal("atestado_medico"),
|
||||
v.literal("declaracao_comparecimento")
|
||||
),
|
||||
dataInicio: v.string(),
|
||||
dataFim: v.string(),
|
||||
cid: v.string(),
|
||||
descricao: v.string(),
|
||||
}),
|
||||
cid: v.optional(v.string()), // Apenas para atestado médico
|
||||
observacoes: v.optional(v.string()),
|
||||
documentoId: v.optional(v.id("_storage")),
|
||||
criadoPor: v.id("usuarios"),
|
||||
criadoEm: v.number(),
|
||||
})
|
||||
.index("by_funcionario", ["funcionarioId"])
|
||||
.index("by_tipo", ["tipo"])
|
||||
.index("by_data_inicio", ["dataInicio"])
|
||||
.index("by_funcionario_and_tipo", ["funcionarioId", "tipo"]),
|
||||
|
||||
licencas: defineTable({
|
||||
funcionarioId: v.id("funcionarios"),
|
||||
tipo: v.union(
|
||||
v.literal("maternidade"),
|
||||
v.literal("paternidade")
|
||||
),
|
||||
dataInicio: v.string(),
|
||||
dataFim: v.string(),
|
||||
documentoId: v.optional(v.id("_storage")),
|
||||
observacoes: v.optional(v.string()),
|
||||
licencaOriginalId: v.optional(v.id("licencas")), // Para prorrogações
|
||||
ehProrrogacao: v.boolean(),
|
||||
criadoPor: v.id("usuarios"),
|
||||
criadoEm: v.number(),
|
||||
})
|
||||
.index("by_funcionario", ["funcionarioId"])
|
||||
.index("by_tipo", ["tipo"])
|
||||
.index("by_data_inicio", ["dataInicio"])
|
||||
.index("by_licenca_original", ["licencaOriginalId"])
|
||||
.index("by_funcionario_and_tipo", ["funcionarioId", "tipo"]),
|
||||
|
||||
solicitacoesFerias: defineTable({
|
||||
funcionarioId: v.id("funcionarios"),
|
||||
|
||||
151
packages/backend/convex/utils/getClientIP.ts
Normal file
151
packages/backend/convex/utils/getClientIP.ts
Normal file
@@ -0,0 +1,151 @@
|
||||
/**
|
||||
* Função utilitária para extrair o IP do cliente de um Request HTTP
|
||||
* Sem usar APIs externas - usa apenas headers HTTP
|
||||
*/
|
||||
|
||||
/**
|
||||
* Extrai o IP do cliente de um Request HTTP
|
||||
* Considera headers como X-Forwarded-For, X-Real-IP, etc.
|
||||
*/
|
||||
export function getClientIP(request: Request): string | undefined {
|
||||
// Headers que podem conter o IP do cliente (case-insensitive)
|
||||
const getHeader = (name: string): string | null => {
|
||||
// Tentar diferentes variações de case
|
||||
const variations = [
|
||||
name,
|
||||
name.toLowerCase(),
|
||||
name.toUpperCase(),
|
||||
name.charAt(0).toUpperCase() + name.slice(1).toLowerCase(),
|
||||
];
|
||||
|
||||
for (const variation of variations) {
|
||||
const value = request.headers.get(variation);
|
||||
if (value) return value;
|
||||
}
|
||||
|
||||
// As variações de case já cobrem a maioria dos casos
|
||||
// Se não encontrou, retorna null
|
||||
return null;
|
||||
};
|
||||
|
||||
const forwardedFor = getHeader("x-forwarded-for");
|
||||
const realIP = getHeader("x-real-ip");
|
||||
const cfConnectingIP = getHeader("cf-connecting-ip"); // Cloudflare
|
||||
const trueClientIP = getHeader("true-client-ip"); // Cloudflare Enterprise
|
||||
const xClientIP = getHeader("x-client-ip");
|
||||
const forwarded = getHeader("forwarded");
|
||||
const remoteAddr = getHeader("remote-addr");
|
||||
|
||||
// Log para debug
|
||||
console.log("Procurando IP nos headers:", {
|
||||
"x-forwarded-for": forwardedFor,
|
||||
"x-real-ip": realIP,
|
||||
"cf-connecting-ip": cfConnectingIP,
|
||||
"true-client-ip": trueClientIP,
|
||||
"x-client-ip": xClientIP,
|
||||
"forwarded": forwarded,
|
||||
"remote-addr": remoteAddr,
|
||||
});
|
||||
|
||||
// Prioridade: X-Forwarded-For pode conter múltiplos IPs (proxy chain)
|
||||
// O primeiro IP é geralmente o IP original do cliente
|
||||
if (forwardedFor) {
|
||||
const ips = forwardedFor.split(",").map((ip) => ip.trim());
|
||||
// Pegar o primeiro IP válido
|
||||
for (const ip of ips) {
|
||||
if (isValidIP(ip)) {
|
||||
console.log("IP encontrado em X-Forwarded-For:", ip);
|
||||
return ip;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Forwarded header (RFC 7239)
|
||||
if (forwarded) {
|
||||
// Formato: for=192.0.2.60;proto=http;by=203.0.113.43
|
||||
const forMatch = forwarded.match(/for=([^;,\s]+)/i);
|
||||
if (forMatch && forMatch[1]) {
|
||||
const ip = forMatch[1].replace(/^\[|\]$/g, ''); // Remove brackets de IPv6
|
||||
if (isValidIP(ip)) {
|
||||
console.log("IP encontrado em Forwarded:", ip);
|
||||
return ip;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Outros headers com IP único
|
||||
if (realIP && isValidIP(realIP)) {
|
||||
console.log("IP encontrado em X-Real-IP:", realIP);
|
||||
return realIP;
|
||||
}
|
||||
|
||||
if (cfConnectingIP && isValidIP(cfConnectingIP)) {
|
||||
console.log("IP encontrado em CF-Connecting-IP:", cfConnectingIP);
|
||||
return cfConnectingIP;
|
||||
}
|
||||
|
||||
if (trueClientIP && isValidIP(trueClientIP)) {
|
||||
console.log("IP encontrado em True-Client-IP:", trueClientIP);
|
||||
return trueClientIP;
|
||||
}
|
||||
|
||||
if (xClientIP && isValidIP(xClientIP)) {
|
||||
console.log("IP encontrado em X-Client-IP:", xClientIP);
|
||||
return xClientIP;
|
||||
}
|
||||
|
||||
if (remoteAddr && isValidIP(remoteAddr)) {
|
||||
console.log("IP encontrado em Remote-Addr:", remoteAddr);
|
||||
return remoteAddr;
|
||||
}
|
||||
|
||||
// Tentar extrair do URL (último recurso)
|
||||
try {
|
||||
const url = new URL(request.url);
|
||||
// Se o servidor estiver configurado para passar IP via query param
|
||||
const ipFromQuery = url.searchParams.get("ip");
|
||||
if (ipFromQuery && isValidIP(ipFromQuery)) {
|
||||
console.log("IP encontrado em query param:", ipFromQuery);
|
||||
return ipFromQuery;
|
||||
}
|
||||
} catch {
|
||||
// Ignorar erro de parsing do URL
|
||||
}
|
||||
|
||||
console.log("Nenhum IP válido encontrado nos headers");
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Valida se uma string é um endereço IP válido (IPv4 ou IPv6)
|
||||
*/
|
||||
function isValidIP(ip: string): boolean {
|
||||
if (!ip || ip.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validar IPv4
|
||||
const ipv4Regex = /^(\d{1,3}\.){3}\d{1,3}$/;
|
||||
if (ipv4Regex.test(ip)) {
|
||||
const parts = ip.split(".");
|
||||
return parts.every((part) => {
|
||||
const num = parseInt(part, 10);
|
||||
return num >= 0 && num <= 255;
|
||||
});
|
||||
}
|
||||
|
||||
// Validar IPv6 (formato simplificado)
|
||||
const ipv6Regex = /^([0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}$/;
|
||||
if (ipv6Regex.test(ip)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Validar IPv6 comprimido (com ::)
|
||||
const ipv6CompressedRegex = /^([0-9a-fA-F]{0,4}:)*::([0-9a-fA-F]{0,4}:)*[0-9a-fA-F]{0,4}$/;
|
||||
if (ipv6CompressedRegex.test(ip)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user