147 lines
4.1 KiB
TypeScript
147 lines
4.1 KiB
TypeScript
/**
|
|
* Key generation utilities for rate limiting
|
|
* These functions help create consistent and secure rate limit keys
|
|
*/
|
|
|
|
import { getConnInfo } from '@hono/node-server/conninfo';
|
|
import type { Context } from 'hono';
|
|
|
|
function normalizeIp(value: string | null | undefined): string | null {
|
|
const ip = value?.trim();
|
|
|
|
if (!ip) return null;
|
|
|
|
return ip.startsWith('::ffff:') ? ip.slice('::ffff:'.length) : ip;
|
|
}
|
|
|
|
/**
|
|
* Extract real client IP address considering various proxy headers
|
|
*/
|
|
export const extractClientIp = (c: Context): string => {
|
|
// Priority order (most trusted first):
|
|
// 1. CF-Connecting-IP (Cloudflare)
|
|
// 2. X-Real-IP (nginx)
|
|
// 3. X-Forwarded-For (standard, but can be spoofed)
|
|
// 4. Connection IP (least reliable)
|
|
|
|
const cfConnectingIp = normalizeIp(c.req.header('cf-connecting-ip'));
|
|
const realIp = normalizeIp(c.req.header('x-real-ip'));
|
|
const forwardedFor = c.req.header('x-forwarded-for');
|
|
const connInfo = getConnInfo(c);
|
|
const remoteAddress = normalizeIp(connInfo.remote.address);
|
|
|
|
if (cfConnectingIp) {
|
|
return cfConnectingIp;
|
|
}
|
|
|
|
if (realIp) {
|
|
return realIp;
|
|
}
|
|
|
|
if (forwardedFor) {
|
|
// x-forwarded-for can contain multiple IPs: "client, proxy1, proxy2"
|
|
// The first IP is usually the original client
|
|
return normalizeIp(forwardedFor.split(',')[0]) ?? 'unknown';
|
|
}
|
|
|
|
if (remoteAddress) {
|
|
return remoteAddress;
|
|
}
|
|
|
|
// Fallback - this might be a proxy IP
|
|
return 'unknown';
|
|
};
|
|
|
|
/**
|
|
* Generate rate limit key for authenticated users
|
|
*/
|
|
export const userKeyGenerator = (c: Context): string => {
|
|
const user = c.get('user'); // Already extracted by global middleware
|
|
|
|
if (user?.id) {
|
|
return `user:${user.id}`;
|
|
}
|
|
|
|
// No valid auth, fall back to IP
|
|
return `ip:${extractClientIp(c)}`;
|
|
};
|
|
|
|
/**
|
|
* Generate rate limit key for API keys
|
|
*/
|
|
export const apiKeyGenerator = (c: Context): string => {
|
|
const apiKey = c.req.header('X-API-Key');
|
|
|
|
if (apiKey) {
|
|
return `api:${apiKey}`;
|
|
}
|
|
|
|
// Fallback to IP if no API key
|
|
return `ip:${extractClientIp(c)}`;
|
|
};
|
|
|
|
/**
|
|
* Generate rate limit key based on IP only
|
|
*/
|
|
export const ipKeyGenerator = (c: Context): string => {
|
|
return `ip:${extractClientIp(c)}`;
|
|
};
|
|
|
|
/**
|
|
* Generate rate limit key for specific endpoints
|
|
*/
|
|
export const endpointKeyGenerator =
|
|
(endpoint: string) =>
|
|
(c: Context): string => {
|
|
const baseKey = userKeyGenerator(c);
|
|
return `${baseKey}:${endpoint}`;
|
|
};
|
|
|
|
// ─── Agent Document Keys ─────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Redis key for storing extracted document text from AI assistant uploads.
|
|
* TTL: 3600 seconds (1 hour).
|
|
*/
|
|
export const agentDocumentKey = (documentId: string): string => {
|
|
return `agent:document:${documentId}`;
|
|
};
|
|
|
|
/**
|
|
* Rate limit key for agent chat endpoint (per-user).
|
|
*/
|
|
export const agentChatKeyGenerator = (c: Context): string => {
|
|
const baseKey = userKeyGenerator(c);
|
|
return `${baseKey}:agent:chat`;
|
|
};
|
|
|
|
// ─── Dashboard / analytics cache ─────────────────────────────────────────────
|
|
|
|
/**
|
|
* Redis key for `getStudentLoginActivity` (day-of-week chart per org and window).
|
|
* Value: JSON array `{ day, count }[]`. TTL: 24h.
|
|
*/
|
|
export function dashLoginActivityKey(orgId: string, days: number): string {
|
|
return `dash:login-activity:${orgId}:${days}`;
|
|
}
|
|
|
|
/**
|
|
* Rate limit key for agent upload endpoint (per-user).
|
|
*/
|
|
export const agentUploadKeyGenerator = (c: Context): string => {
|
|
const baseKey = userKeyGenerator(c);
|
|
return `${baseKey}:agent:upload`;
|
|
};
|
|
|
|
/**
|
|
* Redis keys for engagement analytics read endpoints (landing-stats, funnel,
|
|
* country breakdown, time-to-enrollment). Value: JSON.
|
|
*/
|
|
export function dashAnalyticsKey(route: string, orgId: string, days: number, extra?: string): string {
|
|
const suffix = extra ? `:${extra}` : '';
|
|
return `dash:analytics:${route}:${orgId}:${days}${suffix}`;
|
|
}
|
|
|
|
/** TTL for engagement analytics caches (10 min). */
|
|
export const DASH_ANALYTICS_TTL_SECONDS = 600;
|