2025-02-07 22:25:55 +01:00

106 lines
3.3 KiB
JavaScript

import config from 'config';
import crypto from 'crypto';
import { isbot } from 'isbot';
import getClientIp from '../helpers/client-ip.js';
import prisma from '../services/prisma.js';
const { enabled, ipSalt } = config.get('analytics');
function createUniqueId(ip, userAgent) {
// Use HMAC for secure hashing
return crypto
.createHmac('sha256', ipSalt)
.update(ip + userAgent)
.digest('hex');
}
// Validate path to prevent malicious inputs
function isValidPath(path) {
// Only allow paths that start with / and contain safe characters
const pathRegex = /^\/[a-zA-Z0-9\-_/]*$/;
return pathRegex.test(path) && path.length <= 255 && !path.includes('/secret');
}
async function analytics(fastify) {
fastify.post(
'/track',
{
schema: {
body: {
type: 'object',
required: ['path'],
properties: {
path: { type: 'string', maxLength: 255 },
referrer: { type: 'string', maxLength: 1024 },
},
},
},
},
async (request, reply) => {
if (!enabled) {
return reply.code(403).send({ success: false });
}
try {
const { path } = request.body;
const userAgent = request.headers['user-agent'];
const uniqueId = createUniqueId(getClientIp(request.headers), userAgent);
if (isbot(userAgent)) {
return reply.code(403).send({ success: false });
}
// Validate path
if (!isValidPath(path)) {
return reply.code(400).send({ error: 'Invalid path format' });
}
await prisma.visitorAnalytics.create({
data: {
path,
uniqueId,
},
});
return reply.code(201).send({ success: true });
} catch (error) {
console.error('Analytics tracking error:', error);
return reply.code(500).send({ error: 'Failed to track analytics' });
}
}
);
// Endpoint to get analytics data (protected, admin only)
fastify.get(
'/data',
{
preValidation: [fastify.authenticate],
},
async (request, reply) => {
try {
const user = await prisma.user.findFirst({
where: { username: request.user.username },
});
if (user.role !== 'admin') {
return reply.code(403).send({ error: 'Unauthorized' });
}
const analytics = await prisma.visitorAnalytics.findMany({
orderBy: {
timestamp: 'desc',
},
take: 1000, // Limit to last 1000 entries
});
return reply.send(analytics);
} catch (error) {
console.error('Analytics retrieval error:', error);
return reply.code(500).send({ error: 'Failed to retrieve analytics' });
}
}
);
}
export default analytics;