feat: parse ip location and storage in db
This commit is contained in:
parent
c049a62493
commit
99a6c91b1b
@ -80,6 +80,9 @@ async function findSession(req: Request, url: string) {
|
|||||||
subdivision1,
|
subdivision1,
|
||||||
subdivision2,
|
subdivision2,
|
||||||
city,
|
city,
|
||||||
|
longitude,
|
||||||
|
latitude,
|
||||||
|
accuracyRadius,
|
||||||
} = await getRequestInfo(req);
|
} = await getRequestInfo(req);
|
||||||
|
|
||||||
const sessionId = hashUuid(workspaceId, hostname, ip, userAgent!);
|
const sessionId = hashUuid(workspaceId, hostname, ip, userAgent!);
|
||||||
@ -99,6 +102,9 @@ async function findSession(req: Request, url: string) {
|
|||||||
subdivision1,
|
subdivision1,
|
||||||
subdivision2,
|
subdivision2,
|
||||||
city,
|
city,
|
||||||
|
longitude,
|
||||||
|
latitude,
|
||||||
|
accuracyRadius,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
|
@ -26,21 +26,11 @@ export interface WebsiteEventPayload {
|
|||||||
name?: string;
|
name?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function findSession(req: Request): Promise<{
|
export async function findSession(req: Request): Promise<
|
||||||
id: any;
|
WebsiteSession & {
|
||||||
websiteId: string;
|
|
||||||
hostname: string;
|
|
||||||
browser: string;
|
|
||||||
os: any;
|
|
||||||
device: string;
|
|
||||||
screen: string;
|
|
||||||
language: string;
|
|
||||||
country: any;
|
|
||||||
subdivision1: any;
|
|
||||||
subdivision2: any;
|
|
||||||
city: any;
|
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
}> {
|
}
|
||||||
|
> {
|
||||||
// Verify payload
|
// Verify payload
|
||||||
const { payload } = req.body;
|
const { payload } = req.body;
|
||||||
|
|
||||||
@ -88,6 +78,9 @@ export async function findSession(req: Request): Promise<{
|
|||||||
subdivision1,
|
subdivision1,
|
||||||
subdivision2,
|
subdivision2,
|
||||||
city,
|
city,
|
||||||
|
longitude,
|
||||||
|
latitude,
|
||||||
|
accuracyRadius,
|
||||||
device,
|
device,
|
||||||
} = await getClientInfo(req, payload);
|
} = await getClientInfo(req, payload);
|
||||||
|
|
||||||
@ -114,6 +107,9 @@ export async function findSession(req: Request): Promise<{
|
|||||||
subdivision1,
|
subdivision1,
|
||||||
subdivision2,
|
subdivision2,
|
||||||
city,
|
city,
|
||||||
|
longitude,
|
||||||
|
latitude,
|
||||||
|
accuracyRadius,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
|
@ -12,7 +12,8 @@
|
|||||||
"db:migrate:dev": "prisma migrate dev",
|
"db:migrate:dev": "prisma migrate dev",
|
||||||
"db:migrate:apply": "prisma migrate deploy",
|
"db:migrate:apply": "prisma migrate deploy",
|
||||||
"db:studio": "prisma studio",
|
"db:studio": "prisma studio",
|
||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
"test": "vitest",
|
||||||
|
"test:ci": "vitest --run"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "moonrailgun <moonrailgun@gmail.com>",
|
"author": "moonrailgun <moonrailgun@gmail.com>",
|
||||||
|
@ -0,0 +1,9 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "TelemetrySession" ADD COLUMN "accuracyRadius" INTEGER,
|
||||||
|
ADD COLUMN "latitude" DOUBLE PRECISION,
|
||||||
|
ADD COLUMN "longitude" DOUBLE PRECISION;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "WebsiteSession" ADD COLUMN "accuracyRadius" INTEGER,
|
||||||
|
ADD COLUMN "latitude" DOUBLE PRECISION,
|
||||||
|
ADD COLUMN "longitude" DOUBLE PRECISION;
|
@ -108,6 +108,9 @@ model WebsiteSession {
|
|||||||
subdivision1 String? @db.VarChar(20)
|
subdivision1 String? @db.VarChar(20)
|
||||||
subdivision2 String? @db.VarChar(50)
|
subdivision2 String? @db.VarChar(50)
|
||||||
city String? @db.VarChar(50)
|
city String? @db.VarChar(50)
|
||||||
|
longitude Float?
|
||||||
|
latitude Float?
|
||||||
|
accuracyRadius Int?
|
||||||
createdAt DateTime @default(now()) @db.Timestamptz(6)
|
createdAt DateTime @default(now()) @db.Timestamptz(6)
|
||||||
|
|
||||||
website Website @relation(fields: [websiteId], references: [id], onUpdate: Cascade, onDelete: Cascade)
|
website Website @relation(fields: [websiteId], references: [id], onUpdate: Cascade, onDelete: Cascade)
|
||||||
@ -208,6 +211,9 @@ model TelemetrySession {
|
|||||||
subdivision1 String? @db.VarChar(20)
|
subdivision1 String? @db.VarChar(20)
|
||||||
subdivision2 String? @db.VarChar(50)
|
subdivision2 String? @db.VarChar(50)
|
||||||
city String? @db.VarChar(50)
|
city String? @db.VarChar(50)
|
||||||
|
longitude Float?
|
||||||
|
latitude Float?
|
||||||
|
accuracyRadius Int?
|
||||||
createdAt DateTime @default(now()) @db.Timestamptz(6)
|
createdAt DateTime @default(now()) @db.Timestamptz(6)
|
||||||
|
|
||||||
telemetryEvent TelemetryEvent[]
|
telemetryEvent TelemetryEvent[]
|
||||||
|
@ -13,6 +13,9 @@ export const TelemetrySessionModelSchema = z.object({
|
|||||||
subdivision1: z.string().nullish(),
|
subdivision1: z.string().nullish(),
|
||||||
subdivision2: z.string().nullish(),
|
subdivision2: z.string().nullish(),
|
||||||
city: z.string().nullish(),
|
city: z.string().nullish(),
|
||||||
|
longitude: z.number().nullish(),
|
||||||
|
latitude: z.number().nullish(),
|
||||||
|
accuracyRadius: z.number().int().nullish(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -16,6 +16,9 @@ export const WebsiteSessionModelSchema = z.object({
|
|||||||
subdivision1: z.string().nullish(),
|
subdivision1: z.string().nullish(),
|
||||||
subdivision2: z.string().nullish(),
|
subdivision2: z.string().nullish(),
|
||||||
city: z.string().nullish(),
|
city: z.string().nullish(),
|
||||||
|
longitude: z.number().nullish(),
|
||||||
|
latitude: z.number().nullish(),
|
||||||
|
accuracyRadius: z.number().int().nullish(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
24
src/server/utils/__tests__/detect.test.ts
Normal file
24
src/server/utils/__tests__/detect.test.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { describe, expect, test } from 'vitest';
|
||||||
|
import { getLocation } from '../detect';
|
||||||
|
|
||||||
|
describe('detect', () => {
|
||||||
|
describe('getLocation', () => {
|
||||||
|
test('should detect local ip', async () => {
|
||||||
|
const location = await getLocation('127.0.0.1');
|
||||||
|
|
||||||
|
expect(location).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should detect public ip', async () => {
|
||||||
|
const location = await getLocation('76.76.21.123');
|
||||||
|
|
||||||
|
expect(location).toHaveProperty('country', 'US');
|
||||||
|
expect(location).toHaveProperty('subdivision1', 'CA');
|
||||||
|
expect(location).toHaveProperty('subdivision2', undefined);
|
||||||
|
expect(location).toHaveProperty('city', 'Walnut');
|
||||||
|
expect(location).toHaveProperty('longitude', -117.8512);
|
||||||
|
expect(location).toHaveProperty('latitude', 34.0233);
|
||||||
|
expect(location).toHaveProperty('accuracy_radius', 20);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -2,7 +2,6 @@ import { Request } from 'express';
|
|||||||
import type { WebsiteEventPayload } from '../model/website';
|
import type { WebsiteEventPayload } from '../model/website';
|
||||||
import { getClientIp } from 'request-ip';
|
import { getClientIp } from 'request-ip';
|
||||||
import isLocalhost from 'is-localhost-ip';
|
import isLocalhost from 'is-localhost-ip';
|
||||||
import { safeDecodeURIComponent } from './common';
|
|
||||||
import { browserName, detectOS, OperatingSystem } from 'detect-browser';
|
import { browserName, detectOS, OperatingSystem } from 'detect-browser';
|
||||||
import {
|
import {
|
||||||
DESKTOP_OS,
|
DESKTOP_OS,
|
||||||
@ -14,16 +13,19 @@ import {
|
|||||||
import maxmind, { Reader, CityResponse } from 'maxmind';
|
import maxmind, { Reader, CityResponse } from 'maxmind';
|
||||||
import { libraryPath } from './lib';
|
import { libraryPath } from './lib';
|
||||||
|
|
||||||
let lookup: Reader<CityResponse>;
|
|
||||||
|
|
||||||
export async function getRequestInfo(req: Request) {
|
export async function getRequestInfo(req: Request) {
|
||||||
const userAgent = req.headers['user-agent'];
|
const userAgent = req.headers['user-agent'];
|
||||||
const ip = getIpAddress(req);
|
const ip = getIpAddress(req);
|
||||||
const location = await getLocation(ip, req);
|
const location = await getLocation(ip);
|
||||||
const country = location?.country;
|
const {
|
||||||
const subdivision1 = location?.subdivision1;
|
country,
|
||||||
const subdivision2 = location?.subdivision2;
|
subdivision1,
|
||||||
const city = location?.city;
|
subdivision2,
|
||||||
|
city,
|
||||||
|
longitude,
|
||||||
|
latitude,
|
||||||
|
accuracyRadius,
|
||||||
|
} = location ?? {};
|
||||||
const browser = browserName(userAgent ?? '');
|
const browser = browserName(userAgent ?? '');
|
||||||
const os = detectOS(userAgent ?? '');
|
const os = detectOS(userAgent ?? '');
|
||||||
|
|
||||||
@ -36,6 +38,9 @@ export async function getRequestInfo(req: Request) {
|
|||||||
subdivision1,
|
subdivision1,
|
||||||
subdivision2,
|
subdivision2,
|
||||||
city,
|
city,
|
||||||
|
longitude,
|
||||||
|
latitude,
|
||||||
|
accuracyRadius,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -68,40 +73,13 @@ export function getIpAddress(req: Request): string {
|
|||||||
return getClientIp(req)!;
|
return getClientIp(req)!;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getLocation(ip: string, req: Request) {
|
let lookup: Reader<CityResponse>;
|
||||||
|
export async function getLocation(ip: string) {
|
||||||
// Ignore local ips
|
// Ignore local ips
|
||||||
if (await isLocalhost(ip)) {
|
if (await isLocalhost(ip)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cloudflare headers
|
|
||||||
if (req.headers['cf-ipcountry']) {
|
|
||||||
const country = safeDecodeURIComponent(req.headers['cf-ipcountry']);
|
|
||||||
const subdivision1 = safeDecodeURIComponent(req.headers['cf-region-code']);
|
|
||||||
const city = safeDecodeURIComponent(req.headers['cf-ipcity']);
|
|
||||||
|
|
||||||
return {
|
|
||||||
country,
|
|
||||||
subdivision1: getRegionCode(country, subdivision1),
|
|
||||||
city,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Vercel headers
|
|
||||||
if (req.headers['x-vercel-ip-country']) {
|
|
||||||
const country = safeDecodeURIComponent(req.headers['x-vercel-ip-country']);
|
|
||||||
const subdivision1 = safeDecodeURIComponent(
|
|
||||||
req.headers['x-vercel-ip-country-region']
|
|
||||||
);
|
|
||||||
const city = safeDecodeURIComponent(req.headers['x-vercel-ip-city']);
|
|
||||||
|
|
||||||
return {
|
|
||||||
country,
|
|
||||||
subdivision1: getRegionCode(country, subdivision1),
|
|
||||||
city,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Database lookup
|
// Database lookup
|
||||||
if (!lookup) {
|
if (!lookup) {
|
||||||
lookup = await maxmind.open(libraryPath.geoPath);
|
lookup = await maxmind.open(libraryPath.geoPath);
|
||||||
@ -109,14 +87,19 @@ export async function getLocation(ip: string, req: Request) {
|
|||||||
|
|
||||||
const result = lookup.get(ip);
|
const result = lookup.get(ip);
|
||||||
|
|
||||||
if (result) {
|
if (!result) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
country: result.country?.iso_code ?? result?.registered_country?.iso_code,
|
country: result.country?.iso_code ?? result?.registered_country?.iso_code,
|
||||||
subdivision1: result.subdivisions?.[0]?.iso_code,
|
subdivision1: result.subdivisions?.[0]?.iso_code,
|
||||||
subdivision2: result.subdivisions?.[1]?.names?.en,
|
subdivision2: result.subdivisions?.[1]?.names?.en,
|
||||||
city: result.city?.names?.en,
|
city: result.city?.names?.en,
|
||||||
|
longitude: result.location?.longitude,
|
||||||
|
latitude: result.location?.latitude,
|
||||||
|
accuracyRadius: result.location?.accuracy_radius,
|
||||||
};
|
};
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getRegionCode(
|
function getRegionCode(
|
||||||
|
Loading…
Reference in New Issue
Block a user