import { randomUUID } from "node:crypto"; import { getConfig } from "./config.js"; import { getDb } from "./db.js"; const COOKIE_NAME = "arena_visitor_id"; const COOKIE_MAX_AGE_SECONDS = 60 * 60 * 24 * 365 * 2; const DEFAULT_COLLECTION_NAME = "visitors"; const USER_AGENT_LIMIT = 500; let indexesReady; export async function visitorRoutes(fastify) { fastify.post("/check", async (request, reply) => { return recordVisitor(request, reply); }); fastify.get("/stats", async () => { const collection = await getVisitorCollection(); await ensureVisitorIndexes(collection); return { uniqueVisitors: await collection.countDocuments(), }; }); } async function recordVisitor(request, reply) { const collection = await getVisitorCollection(); await ensureVisitorIndexes(collection); let visitorId = readCookie(request, COOKIE_NAME); const hadValidCookie = isValidVisitorId(visitorId); if (!hadValidCookie) { visitorId = randomUUID(); } const now = new Date(); const userAgent = String(request.headers["user-agent"] || "").slice(0, USER_AGENT_LIMIT); const result = await collection.updateOne( { _id: visitorId }, { $setOnInsert: { _id: visitorId, firstSeenAt: now, firstUserAgent: userAgent, }, $set: { lastSeenAt: now, lastUserAgent: userAgent, }, $inc: { visits: 1, }, }, { upsert: true }, ); if (!hadValidCookie || result.upsertedCount > 0) { writeVisitorCookie(reply, visitorId); } return { isNewVisitor: result.upsertedCount > 0, uniqueVisitors: await collection.countDocuments(), checkedAt: now.toISOString(), }; } async function getVisitorCollection() { const db = await getDb(); return db.collection(getConfig().MONGODB_VISITOR_COLLECTION || DEFAULT_COLLECTION_NAME); } async function ensureVisitorIndexes(collection) { if (!indexesReady) { indexesReady = collection.createIndex({ lastSeenAt: -1 }); } return indexesReady; } function readCookie(request, name) { const cookieHeader = request.headers.cookie; if (!cookieHeader) { return ""; } const cookies = cookieHeader.split(";").map((cookie) => cookie.trim()); const matchedCookie = cookies.find((cookie) => cookie.startsWith(`${name}=`)); if (!matchedCookie) { return ""; } try { return decodeURIComponent(matchedCookie.slice(name.length + 1)); } catch { return ""; } } function writeVisitorCookie(reply, visitorId) { const secureFlag = getConfig().COOKIE_SECURE ? "; Secure" : ""; reply.header( "Set-Cookie", `${COOKIE_NAME}=${encodeURIComponent(visitorId)}; Path=/; Max-Age=${COOKIE_MAX_AGE_SECONDS}; SameSite=Lax; HttpOnly${secureFlag}`, ); } function isValidVisitorId(value) { return /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test( value, ); }