import { randomUUID } from "node:crypto"; import { getConfig } from "./config.js"; import { getDb } from "./db.js"; import { recordDailyVisit } from "./dailyMetrics.js"; import { isValidVisitorId, readVisitorCookie, writeVisitorCookie } from "./visitorCookie.js"; 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 = readVisitorCookie(request); 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, { secure: getConfig().COOKIE_SECURE }); } try { await recordDailyVisit(visitorId, { now }); } catch (error) { request.log.warn({ err: error }, "Daily visit metrics update failed."); } 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; }