246 lines
4.3 KiB
TypeScript
246 lines
4.3 KiB
TypeScript
import { db } from "@/lib/db";
|
|
import { geoCache } from "@/lib/db/schema";
|
|
import { eq } from "drizzle-orm";
|
|
|
|
interface GeoLocation {
|
|
latitude: number;
|
|
longitude: number;
|
|
city: string;
|
|
country: string;
|
|
countryCode: string;
|
|
region: string;
|
|
}
|
|
|
|
interface IpApiResponse {
|
|
status: string;
|
|
lat: number;
|
|
lon: number;
|
|
city: string;
|
|
country: string;
|
|
countryCode: string;
|
|
}
|
|
|
|
const CONTINENT_MAP: Record<string, string> = {
|
|
// Asia
|
|
CN: "Asia",
|
|
JP: "Asia",
|
|
KR: "Asia",
|
|
IN: "Asia",
|
|
ID: "Asia",
|
|
TH: "Asia",
|
|
VN: "Asia",
|
|
PH: "Asia",
|
|
MY: "Asia",
|
|
SG: "Asia",
|
|
TW: "Asia",
|
|
HK: "Asia",
|
|
BD: "Asia",
|
|
PK: "Asia",
|
|
LK: "Asia",
|
|
NP: "Asia",
|
|
MM: "Asia",
|
|
KH: "Asia",
|
|
LA: "Asia",
|
|
MN: "Asia",
|
|
KZ: "Asia",
|
|
UZ: "Asia",
|
|
AE: "Asia",
|
|
SA: "Asia",
|
|
IL: "Asia",
|
|
TR: "Asia",
|
|
IQ: "Asia",
|
|
IR: "Asia",
|
|
QA: "Asia",
|
|
KW: "Asia",
|
|
BH: "Asia",
|
|
OM: "Asia",
|
|
JO: "Asia",
|
|
LB: "Asia",
|
|
AF: "Asia",
|
|
|
|
// Europe
|
|
GB: "Europe",
|
|
DE: "Europe",
|
|
FR: "Europe",
|
|
IT: "Europe",
|
|
ES: "Europe",
|
|
PT: "Europe",
|
|
NL: "Europe",
|
|
BE: "Europe",
|
|
SE: "Europe",
|
|
NO: "Europe",
|
|
DK: "Europe",
|
|
FI: "Europe",
|
|
PL: "Europe",
|
|
CZ: "Europe",
|
|
AT: "Europe",
|
|
CH: "Europe",
|
|
IE: "Europe",
|
|
RO: "Europe",
|
|
HU: "Europe",
|
|
GR: "Europe",
|
|
UA: "Europe",
|
|
RU: "Europe",
|
|
BG: "Europe",
|
|
HR: "Europe",
|
|
SK: "Europe",
|
|
SI: "Europe",
|
|
RS: "Europe",
|
|
LT: "Europe",
|
|
LV: "Europe",
|
|
EE: "Europe",
|
|
IS: "Europe",
|
|
LU: "Europe",
|
|
MT: "Europe",
|
|
CY: "Europe",
|
|
AL: "Europe",
|
|
MK: "Europe",
|
|
BA: "Europe",
|
|
ME: "Europe",
|
|
MD: "Europe",
|
|
BY: "Europe",
|
|
|
|
// Americas
|
|
US: "Americas",
|
|
CA: "Americas",
|
|
MX: "Americas",
|
|
BR: "Americas",
|
|
AR: "Americas",
|
|
CO: "Americas",
|
|
CL: "Americas",
|
|
PE: "Americas",
|
|
VE: "Americas",
|
|
EC: "Americas",
|
|
BO: "Americas",
|
|
PY: "Americas",
|
|
UY: "Americas",
|
|
CR: "Americas",
|
|
PA: "Americas",
|
|
CU: "Americas",
|
|
DO: "Americas",
|
|
GT: "Americas",
|
|
HN: "Americas",
|
|
SV: "Americas",
|
|
NI: "Americas",
|
|
JM: "Americas",
|
|
TT: "Americas",
|
|
HT: "Americas",
|
|
PR: "Americas",
|
|
GY: "Americas",
|
|
SR: "Americas",
|
|
BZ: "Americas",
|
|
|
|
// Africa
|
|
ZA: "Africa",
|
|
NG: "Africa",
|
|
KE: "Africa",
|
|
EG: "Africa",
|
|
GH: "Africa",
|
|
ET: "Africa",
|
|
TZ: "Africa",
|
|
MA: "Africa",
|
|
DZ: "Africa",
|
|
TN: "Africa",
|
|
UG: "Africa",
|
|
SN: "Africa",
|
|
CI: "Africa",
|
|
CM: "Africa",
|
|
MZ: "Africa",
|
|
MG: "Africa",
|
|
AO: "Africa",
|
|
ZW: "Africa",
|
|
RW: "Africa",
|
|
LY: "Africa",
|
|
SD: "Africa",
|
|
CD: "Africa",
|
|
ML: "Africa",
|
|
BF: "Africa",
|
|
NE: "Africa",
|
|
MW: "Africa",
|
|
ZM: "Africa",
|
|
BW: "Africa",
|
|
NA: "Africa",
|
|
MU: "Africa",
|
|
|
|
// Oceania
|
|
AU: "Oceania",
|
|
NZ: "Oceania",
|
|
FJ: "Oceania",
|
|
PG: "Oceania",
|
|
WS: "Oceania",
|
|
TO: "Oceania",
|
|
VU: "Oceania",
|
|
SB: "Oceania",
|
|
GU: "Oceania",
|
|
NC: "Oceania",
|
|
PF: "Oceania",
|
|
};
|
|
|
|
function getRegion(countryCode: string): string {
|
|
return CONTINENT_MAP[countryCode] ?? "Unknown";
|
|
}
|
|
|
|
export async function getGeoLocation(
|
|
ip: string
|
|
): Promise<GeoLocation | null> {
|
|
try {
|
|
const cached = await db
|
|
.select()
|
|
.from(geoCache)
|
|
.where(eq(geoCache.ip, ip))
|
|
.limit(1);
|
|
|
|
if (cached.length > 0) {
|
|
const row = cached[0];
|
|
return {
|
|
latitude: Number(row.latitude),
|
|
longitude: Number(row.longitude),
|
|
city: row.city ?? "",
|
|
country: row.country ?? "",
|
|
countryCode: row.countryCode ?? "",
|
|
region: row.region ?? "",
|
|
};
|
|
}
|
|
|
|
const response = await fetch(`http://ip-api.com/json/${ip}`, {
|
|
signal: AbortSignal.timeout(5000),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
return null;
|
|
}
|
|
|
|
const data: IpApiResponse = await response.json();
|
|
|
|
if (data.status !== "success") {
|
|
return null;
|
|
}
|
|
|
|
const region = getRegion(data.countryCode);
|
|
|
|
const geoData: GeoLocation = {
|
|
latitude: data.lat,
|
|
longitude: data.lon,
|
|
city: data.city,
|
|
country: data.country,
|
|
countryCode: data.countryCode,
|
|
region,
|
|
};
|
|
|
|
await db.insert(geoCache).values({
|
|
ip,
|
|
latitude: String(geoData.latitude),
|
|
longitude: String(geoData.longitude),
|
|
city: geoData.city,
|
|
country: geoData.country,
|
|
countryCode: geoData.countryCode,
|
|
region: geoData.region,
|
|
});
|
|
|
|
return geoData;
|
|
} catch (error) {
|
|
console.error("Failed to get geo location for IP:", ip, error);
|
|
return null;
|
|
}
|
|
}
|