Skip to content

Commit

Permalink
Adjust API format and wire up to Jetstream session api
Browse files Browse the repository at this point in the history
Based on env var, call new service instead of old service

Output format was modified to match prior data structure to make the migration seamless
  • Loading branch information
paustint committed Dec 22, 2024
1 parent c6bb45e commit 0800bbb
Show file tree
Hide file tree
Showing 5 changed files with 78 additions and 61 deletions.
2 changes: 1 addition & 1 deletion apps/geo-ip-api/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ app.post(
return {
ipAddress,
isValid,
results: isValid ? lookupIpAddress(ipAddress) : null,
...(isValid ? lookupIpAddress(ipAddress) : null),
};
});

Expand Down
82 changes: 44 additions & 38 deletions apps/geo-ip-api/src/maxmind.service.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { logger } from '@jetstream/api-config';
import fs from 'fs';
import maxMind, { CityResponse, Reader } from 'maxmind';
import maxMind, { AsnResponse, CityResponse, Reader } from 'maxmind';
import path from 'path';
import * as tar from 'tar';
import { promisify } from 'util';
Expand All @@ -10,26 +10,33 @@ const writeFileAsync = promisify(fs.writeFile);
const statAsync = promisify(fs.stat);
const mkdirAsync = promisify(fs.mkdir);

// const ASN_URL = 'https://download.maxmind.com/geoip/databases/GeoLite2-ASN/download?suffix=tar.gz';
// const ASN_FILENAME = 'GeoLite2-ASN.tar.gz';
const ASN_URL = 'https://download.maxmind.com/geoip/databases/GeoLite2-ASN/download?suffix=tar.gz';
const ASN_FILENAME = 'GeoLite2-ASN.tar.gz';
const ASN_DB_FILENAME = 'GeoLite2-ASN.mmdb';

const FOLDER_NAME = 'downloads';
const CITY_URL = 'https://download.maxmind.com/geoip/databases/GeoLite2-City/download?suffix=tar.gz';
const CITY_ZIP_FILENAME = 'GeoLite2-City.tar.gz';
const CITY_DB_FILENAME = 'GeoLite2-City.mmdb';

const downloads = [
// { url: ASN_URL, filename: ASN_FILENAME },
{ url: CITY_URL, archiveFilename: CITY_ZIP_FILENAME, dbFileName: CITY_DB_FILENAME, defaultLookup: true },
{ url: ASN_URL, archiveFilename: ASN_FILENAME, dbFileName: ASN_DB_FILENAME },
{ url: CITY_URL, archiveFilename: CITY_ZIP_FILENAME, dbFileName: CITY_DB_FILENAME },
];

let lookup: Reader<CityResponse>;
let lookupAsn: Reader<AsnResponse>;
let lookupCity: Reader<CityResponse>;

export async function initMaxMind(rootDir: string, force = false) {
if (force || !lookup) {
if (force || !lookupAsn) {
const filePath = path.join(rootDir, FOLDER_NAME, ASN_DB_FILENAME);
logger.info(`Initializing ASN database: ${filePath}`);
lookupAsn = await maxMind.open<AsnResponse>(filePath);
}
if (force || !lookupCity) {
const filePath = path.join(rootDir, FOLDER_NAME, CITY_DB_FILENAME);
logger.info(`Initializing database: ${filePath}`);
lookup = await maxMind.open<CityResponse>(filePath);
logger.info(`Initializing CITY database: ${filePath}`);
lookupCity = await maxMind.open<CityResponse>(filePath);
}
}

Expand All @@ -42,7 +49,7 @@ export async function downloadMaxMindDb(rootDir: string): Promise<void> {
await mkdirAsync(downloadFolderPath, { recursive: true });
}

for (const { defaultLookup, archiveFilename, url } of downloads) {
for (const { archiveFilename, url } of downloads) {
const archiveFilePath = path.join(downloadFolderPath, archiveFilename);

// Check if file needs to be downloaded
Expand Down Expand Up @@ -84,43 +91,42 @@ export async function downloadMaxMindDb(rootDir: string): Promise<void> {
filter: (path) => path.endsWith('.mmdb'),
strip: 1, // Remove the first directory component from paths
});

if (defaultLookup) {
initMaxMind(downloadFolderPath, true);
}
}
await initMaxMind(rootDir, true);
}

export function lookupIpAddress(ipAddress: string) {
if (!lookup) {
if (!lookupAsn || !lookupCity) {
throw new Error('MaxMind DB not initialized');
}

const results = lookup.get(ipAddress);
if (!results) {
return null;
const asnResults = lookupAsn.get(ipAddress);
const cityResults = lookupCity.get(ipAddress);
if (!cityResults) {
return {
query: ipAddress,
status: 'fail',
};
}

return {
city: results.city?.names?.en ?? null,
country: results.country?.names?.en ?? null,
countryISO: results.country?.iso_code ?? null,
isEU: !!results.country?.is_in_european_union,
continent: results.continent?.names?.en ?? null,
location: results.location ?? null,
postalCode: results.postal?.code ?? null,
registeredCountry: results.registered_country
? {
country: results.registered_country.names.en,
iso: results.registered_country.iso_code,
isEU: !!results.registered_country.is_in_european_union,
}
: null,
subdivisions:
results.subdivisions?.map((item) => ({
name: item.names.en,
iso: item.iso_code,
})) ?? [],
foo: results.traits,
query: ipAddress,
status: 'success',
continent: cityResults.continent?.names?.en ?? null,
continentCode: cityResults.continent?.code ?? null,
country: cityResults.country?.names?.en ?? null,
countryCode: cityResults.country?.iso_code ?? null,
region: cityResults.subdivisions?.[0]?.iso_code ?? null,
regionName: cityResults.subdivisions?.[0]?.names?.en ?? null,
city: cityResults.city?.names?.en ?? null,
zip: cityResults.postal?.code ?? null,
lat: cityResults.location?.latitude ?? null,
lon: cityResults.location?.longitude ?? null,
timezone: cityResults.location?.time_zone ?? null,
isEU: !!cityResults.country?.is_in_european_union,
isp: cityResults.traits?.isp ?? null,
org: cityResults.traits?.organization ?? asnResults?.autonomous_system_organization ?? null,
proxy: cityResults.traits?.is_anonymous_proxy ?? false,
};
}

Expand Down
7 changes: 4 additions & 3 deletions libs/api-config/src/lib/env-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ const envSchema = z.object({
CAPTCHA_SECRET_KEY: z.string().optional(),
CAPTCHA_PROPERTY: z.literal('captchaToken').optional().default('captchaToken'),
IP_API_KEY: z.string().optional().describe('API Key used to get location information from IP address'),
IP_API_SERVICE: z.enum(['IP-API', 'LOCAL']).optional().describe('API Key used to get location information from IP address'),
VERSION: z.string().optional(),
ROLLBAR_SERVER_TOKEN: z.string().optional(),

Expand Down Expand Up @@ -204,9 +205,9 @@ const envSchema = z.object({
/**
* GEO-IP API (private service basic auth)
*/
GEO_IP_API_USERNAME: z.string().nullish(),
GEO_IP_API_PASSWORD: z.string().nullish(),
GEO_IP_API_HOSTNAME: z.string().nullish(),
GEO_IP_API_USERNAME: z.string().optional(),
GEO_IP_API_PASSWORD: z.string().optional(),
GEO_IP_API_HOSTNAME: z.string().optional(),
});

const parseResults = envSchema.safeParse({
Expand Down
34 changes: 22 additions & 12 deletions libs/auth/server/src/lib/auth.db.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -283,21 +283,31 @@ export async function getUserSessions(userId: string, omitLocationData?: boolean
);

// Fetch location data and add to each session
if (!omitLocationData && ENV.IP_API_KEY && sessions.length > 0) {
if (!omitLocationData && sessions.length > 0) {
try {
let response: Awaited<ReturnType<typeof fetch>> | null = null;
const ipAddresses = sessions.map((session) => session.ipAddress);
if (ENV.IP_API_SERVICE === 'LOCAL' && ENV.GEO_IP_API_USERNAME && ENV.GEO_IP_API_PASSWORD && ENV.GEO_IP_API_HOSTNAME) {
response = await fetch(`${ENV.GEO_IP_API_HOSTNAME}/api/lookup`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Basic ${Buffer.from(`${ENV.GEO_IP_API_USERNAME}:${ENV.GEO_IP_API_PASSWORD}`, 'utf-8').toString('base64')}`,
},
body: JSON.stringify({ ips: ipAddresses }),
});
} else if (ENV.IP_API_KEY) {
const params = new URLSearchParams({
fields: 'status,country,countryCode,region,regionName,city,isp,query',
key: ENV.IP_API_KEY,
});

const params = new URLSearchParams({
fields: 'status,country,countryCode,region,regionName,city,isp,query',
key: ENV.IP_API_KEY,
});

const response = await fetch(`https://pro.ip-api.com/batch?${params.toString()}`, {
method: 'POST',
body: JSON.stringify(ipAddresses),
});

if (response.ok) {
response = await fetch(`https://pro.ip-api.com/batch?${params.toString()}`, {
method: 'POST',
body: JSON.stringify(ipAddresses),
});
}
if (response?.ok) {
const locations = (await response.json()) as SessionIpData[];
return sessions.map(
(session, i): UserSessionWithLocation => ({
Expand Down
14 changes: 7 additions & 7 deletions libs/auth/types/src/lib/auth-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,13 +106,13 @@ export interface SessionData {

export interface SessionIpSuccess {
status: 'success';
country: string;
countryCode: string;
region: string;
regionName: string;
city: string;
isp: string;
query: string;
country: string | null;
countryCode: string | null;
region: string | null;
regionName: string | null;
city: string | null;
isp: string | null;
query: string | null;
}

export interface SessionIpFail {
Expand Down

0 comments on commit 0800bbb

Please sign in to comment.