import type { FastifyInstance } from 'fastify'; import { z } from 'zod'; import { and, eq, gte, sql } from 'drizzle-orm'; import { getDb, toolUsage } from '@lawdesk/db'; const TOOL_NAMES = [ 'hourly-rate-calculator', 'case-profitability', 'billable-hours-tracker', 'document-templates', ] as const; const logBody = z.object({ tool: z.enum(TOOL_NAMES), sessionId: z.string().max(64).optional(), }); export async function toolUsageRoutes(app: FastifyInstance) { // Log a usage event. Rate-limited per IP so a malicious caller can't pump up "online now" counts. app.post( '/api/tool-usage', { config: { rateLimit: { max: 60, timeWindow: '1 minute' } } }, async (req, reply) => { const parsed = logBody.safeParse(req.body); if (!parsed.success) return reply.code(400).send({ error: 'invalid_tool' }); await getDb().insert(toolUsage).values({ tool: parsed.data.tool, sessionId: parsed.data.sessionId ?? null, ip: req.ip ?? null, }); return { ok: true }; }, ); // Per-tool count of unique sessions in the last 5 minutes — what the public pages display // as "X online". Public route, very cheap query. app.get('/api/tool-usage/online', async () => { const since = new Date(Date.now() - 5 * 60 * 1000); const rows = await getDb() .select({ tool: toolUsage.tool, // Distinct (session_id, ip) so multiple page hits from the same browser don't multi-count count: sql`count(distinct coalesce(${toolUsage.sessionId}, host(${toolUsage.ip}::inet)))::int`, }) .from(toolUsage) .where(gte(toolUsage.createdAt, since)) .groupBy(toolUsage.tool); const map: Record = {}; for (const r of rows) map[r.tool] = r.count; return { online: map, since: since.toISOString() }; }); }