mirror of
https://github.com/Litlyx/litlyx
synced 2025-12-10 07:48:37 +01:00
add broker
This commit is contained in:
20
broker/src/Controller.ts
Normal file
20
broker/src/Controller.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { TProjectCount } from "@schema/ProjectsCounts";
|
||||
import { ProjectModel } from "@schema/ProjectSchema";
|
||||
import { UserModel } from "@schema/UserSchema";
|
||||
import { LimitNotifyModel } from "@schema/broker/LimitNotifySchema";
|
||||
import { sendLimitEmail50 } from '@services/EmailService';
|
||||
|
||||
export async function checkLimitsForEmail(projectCounts: TProjectCount) {
|
||||
|
||||
if ((projectCounts.visits + projectCounts.events) >= (projectCounts.limit / 2)) {
|
||||
const notify = await LimitNotifyModel.findOne({ project_id: projectCounts._id });
|
||||
if (notify && notify.limit1 === true) return;
|
||||
const project = await ProjectModel.findById(projectCounts.project_id);
|
||||
if (!project) return;
|
||||
const owner = await UserModel.findById(project.owner);
|
||||
if (!owner) return;
|
||||
await sendLimitEmail50(owner.email);
|
||||
await LimitNotifyModel.updateOne({ project_id: projectCounts._id }, { limit1: true, limit2: false, limit3: false }, { upsert: true });
|
||||
}
|
||||
|
||||
}
|
||||
16
broker/src/ScreenSize.ts
Normal file
16
broker/src/ScreenSize.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
|
||||
|
||||
export function getDeviceFromScreenSize(width: number, height: number) {
|
||||
const totalArea = width * height;
|
||||
|
||||
const mobileArea = 375 * 667;
|
||||
const tabletMinArea = 768 * 1366
|
||||
const tabletMaxArea = 1024 * 1366
|
||||
|
||||
const isMobile = totalArea <= mobileArea;
|
||||
const isTablet = totalArea >= tabletMinArea && totalArea <= tabletMaxArea;
|
||||
|
||||
if (isMobile) return 'mobile';
|
||||
if (isTablet) return 'tablet'
|
||||
return 'desktop';
|
||||
}
|
||||
17
broker/src/index.ts
Normal file
17
broker/src/index.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import express from 'express';
|
||||
import cors from 'cors';
|
||||
|
||||
import { requireEnv } from '../../shared/utilts/requireEnv';
|
||||
import { connectDatabase } from '@services/DatabaseService';
|
||||
|
||||
const app = express();
|
||||
app.use(cors());
|
||||
|
||||
connectDatabase(requireEnv('MONGO_CONNECTION_STRING'));
|
||||
|
||||
import HealthRouter from './routes/HealthRouter';
|
||||
app.use('/health', HealthRouter);
|
||||
import V1Router from './routes/v1/Router';
|
||||
app.use('/v1', V1Router);
|
||||
|
||||
app.listen(requireEnv('PORT'), () => console.log(`Listening on port ${requireEnv('PORT')}`));
|
||||
42
broker/src/lookup.ts
Normal file
42
broker/src/lookup.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import fs from 'fs';
|
||||
|
||||
const ipsData = JSON.parse(fs.readFileSync('./dist/ipv4-db.json', 'utf8'));
|
||||
const countriesData = JSON.parse(fs.readFileSync('./dist/countries-db.json', 'utf8'));
|
||||
|
||||
function inRange(ip: string, cidr: string) {
|
||||
const [subnet, mask] = cidr.split('/');
|
||||
const ipBytes = ip.split('.').map(Number);
|
||||
const subnetBytes = subnet.split('.').map(Number);
|
||||
|
||||
const ipInt = (ipBytes[0] << 24) | (ipBytes[1] << 16) | (ipBytes[2] << 8) | ipBytes[3];
|
||||
const subnetInt = (subnetBytes[0] << 24) | (subnetBytes[1] << 16) | (subnetBytes[2] << 8) | subnetBytes[3];
|
||||
|
||||
const maskInt = 0xffffffff << (32 - parseInt(mask));
|
||||
|
||||
return (ipInt & maskInt) === (subnetInt & maskInt);
|
||||
}
|
||||
|
||||
function getCountryFromId(id: number) {
|
||||
for (const country of countriesData) {
|
||||
if (country[0] == id) {
|
||||
return country;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function lookup(ip: string) {
|
||||
try {
|
||||
const startPiece = parseInt(ip.split('.')[0]);
|
||||
for (const target of ipsData) {
|
||||
const matchingStartPiece = target[0] == startPiece;
|
||||
if (!matchingStartPiece) continue;
|
||||
if (!inRange(ip, target[1])) continue;
|
||||
const country = getCountryFromId(target[2]);
|
||||
return [country[1], country[2]];
|
||||
}
|
||||
return ['??', '??'];
|
||||
} catch (ex) {
|
||||
console.error('ERROR DURING LOOKUP', ex);
|
||||
return ['??', '??'];
|
||||
}
|
||||
}
|
||||
15
broker/src/routes/HealthRouter.ts
Normal file
15
broker/src/routes/HealthRouter.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
|
||||
import { Router } from "express";
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get('/', async (req, res) => {
|
||||
try {
|
||||
return res.json({ alive: true });
|
||||
} catch (ex) {
|
||||
console.error(ex);
|
||||
return res.status(500).json({ error: 'ERROR' });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
127
broker/src/routes/v1/Router.ts
Normal file
127
broker/src/routes/v1/Router.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
|
||||
import { Router, json } from "express";
|
||||
import { createSessionHash, getIPFromRequest } from "../../utils/Utils";
|
||||
import { checkProjectCount } from "@functions/UtilsProjectCounts";
|
||||
|
||||
import { SessionModel } from "@schema/metrics/SessionSchema";
|
||||
import { EVENT_LOG_LIMIT_PERCENT } from '@data/broker/Limits';
|
||||
import { EventType } from '@data/broker/EventType';
|
||||
import { lookup } from "../../lookup";
|
||||
import { UAParser } from "ua-parser-js";
|
||||
import { getDeviceFromScreenSize } from "../../ScreenSize";
|
||||
import { VisitModel } from "@schema/metrics/VisitSchema";
|
||||
import { EventModel } from "@schema/metrics/EventSchema";
|
||||
import { ProjectCountModel } from "@schema/ProjectsCounts";
|
||||
import { checkLimitsForEmail } from "../../Controller";
|
||||
|
||||
const router = Router();
|
||||
|
||||
const allowAnyType = () => true;
|
||||
const jsonOptions = { limit: '10mb', type: allowAnyType }
|
||||
|
||||
router.post('/keep_alive', json(jsonOptions), async (req, res) => {
|
||||
try {
|
||||
|
||||
const ip = getIPFromRequest(req);
|
||||
|
||||
const { pid, website, userAgent, instant } = req.body;
|
||||
|
||||
const sessionHash = createSessionHash(website, ip, userAgent);
|
||||
|
||||
if (instant == true) {
|
||||
await SessionModel.updateOne({ project_id: pid, session: sessionHash, }, {
|
||||
$inc: { duration: 0 },
|
||||
updated_at: Date.now()
|
||||
}, { upsert: true });
|
||||
} else {
|
||||
await SessionModel.updateOne({ project_id: pid, session: sessionHash, }, {
|
||||
$inc: { duration: 1 },
|
||||
updated_at: Date.now()
|
||||
}, { upsert: true });
|
||||
}
|
||||
|
||||
return res.sendStatus(200);
|
||||
|
||||
} catch (ex) {
|
||||
console.error(ex);
|
||||
return res.status(500).json({ error: 'ERROR' });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
router.post('/metrics/push', json(jsonOptions), async (req, res) => {
|
||||
|
||||
try {
|
||||
|
||||
const { pid } = req.body;
|
||||
|
||||
const projectCounts = await checkProjectCount(pid);
|
||||
|
||||
const TOTAL_COUNT = projectCounts.events + projectCounts.visits;
|
||||
const LIMIT = projectCounts.limit;
|
||||
if ((TOTAL_COUNT * EVENT_LOG_LIMIT_PERCENT) > LIMIT) return;
|
||||
|
||||
await checkLimitsForEmail(projectCounts);
|
||||
|
||||
const ip = getIPFromRequest(req);
|
||||
|
||||
const { type } = req.body;
|
||||
|
||||
if (type === null || type === undefined) return res.status(400).json({ error: 'type is required' });
|
||||
if (typeof type !== 'number') return res.status(400).json({ error: 'type must be a number' });
|
||||
if (type < 0) return res.status(400).json({ error: 'type must be positive' });
|
||||
|
||||
if (type === EventType.VISIT) {
|
||||
const { website, page, referrer, screenWidth, screenHeight, userAgent } = req.body;
|
||||
let referrerParsed;
|
||||
try {
|
||||
referrerParsed = new URL(referrer);
|
||||
} catch (ex) {
|
||||
referrerParsed = { hostname: referrer };
|
||||
}
|
||||
|
||||
const geoLocation = lookup(ip);
|
||||
|
||||
const userAgentParsed = UAParser(userAgent);
|
||||
|
||||
const device = getDeviceFromScreenSize(screenWidth, screenHeight);
|
||||
|
||||
const visit = new VisitModel({
|
||||
project_id: pid, website, page, referrer: referrerParsed.hostname,
|
||||
browser: userAgentParsed.browser.name || 'NO_BROWSER',
|
||||
os: userAgentParsed.os.name || 'NO_OS',
|
||||
device,
|
||||
continent: geoLocation[0],
|
||||
country: geoLocation[1],
|
||||
});
|
||||
|
||||
await visit.save();
|
||||
|
||||
} else {
|
||||
const { name, metadata } = req.body;
|
||||
let metadataObject;
|
||||
try {
|
||||
if (metadata) metadataObject = JSON.parse(metadata);
|
||||
} catch (ex) {
|
||||
metadataObject = { error: 'Error parsing metadata' }
|
||||
}
|
||||
|
||||
const event = new EventModel({ project_id: pid, name, metadata: metadataObject });
|
||||
await event.save();
|
||||
}
|
||||
|
||||
|
||||
const fieldToInc = type === EventType.VISIT ? 'visits' : 'events';
|
||||
|
||||
await ProjectCountModel.updateOne({ _id: projectCounts._id }, { $inc: { [fieldToInc]: 1 } });
|
||||
|
||||
return res.sendStatus(200);
|
||||
|
||||
} catch (ex) {
|
||||
console.error(ex);
|
||||
return res.status(500).json({ error: 'ERROR' });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
export default router;
|
||||
15
broker/src/utils/Utils.ts
Normal file
15
broker/src/utils/Utils.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Request } from "express";
|
||||
import crypto from 'crypto';
|
||||
|
||||
export function getIPFromRequest(req: Request) {
|
||||
const ip = req.header('X-Real-IP') || req.header('X-Forwarded-For') || '0.0.0.0';
|
||||
return ip;
|
||||
}
|
||||
|
||||
|
||||
export function createSessionHash(website: string, ip: string, userAgent: string) {
|
||||
const dailySalt = new Date().toLocaleDateString('it-IT');
|
||||
const sessionClean = dailySalt + website + ip + userAgent;
|
||||
const sessionHash = crypto.createHash('md5').update(sessionClean).digest("hex");
|
||||
return sessionHash;
|
||||
}
|
||||
Reference in New Issue
Block a user