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:
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