89 lines
2.2 KiB
JavaScript
89 lines
2.2 KiB
JavaScript
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;
|
|
}
|
|
|