2025-08-08 15:45:25 +07:00
|
|
|
import { Elysia } from "elysia";
|
|
|
|
|
import { cors } from "@elysiajs/cors";
|
2023-09-24 19:55:50 +07:00
|
|
|
|
2025-08-08 15:45:25 +07:00
|
|
|
const app = new Elysia()
|
|
|
|
|
.use(cors({
|
|
|
|
|
origin: true,
|
|
|
|
|
methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
|
|
|
|
|
credentials: true,
|
|
|
|
|
allowedHeaders: ["Content-Type", "Authorization"]
|
|
|
|
|
}));
|
2023-09-24 19:55:50 +07:00
|
|
|
|
|
|
|
|
enum IpLookupStatus {
|
|
|
|
|
success = "success",
|
|
|
|
|
fail = "fail",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface GeoIpLookupCache {
|
|
|
|
|
[key: string]: {
|
|
|
|
|
status: IpLookupStatus;
|
|
|
|
|
country: string;
|
|
|
|
|
countryCode: string;
|
|
|
|
|
region: string;
|
|
|
|
|
regionName: string;
|
|
|
|
|
city: string;
|
|
|
|
|
zip: string;
|
|
|
|
|
lat: number;
|
|
|
|
|
lon: number;
|
|
|
|
|
timezone: string;
|
|
|
|
|
isp: string;
|
|
|
|
|
org: string;
|
|
|
|
|
as: string;
|
|
|
|
|
query: string;
|
|
|
|
|
cacheExpireAt: Date;
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface WeatherLookupCache {
|
|
|
|
|
[key: string]: {
|
|
|
|
|
latitude: number;
|
|
|
|
|
longitude: number;
|
|
|
|
|
generationtime_ms: number;
|
|
|
|
|
utc_offset_seconds: number;
|
|
|
|
|
timezone: string;
|
|
|
|
|
timezone_abbreviation: string;
|
|
|
|
|
elevation: number;
|
|
|
|
|
current_weather: {
|
|
|
|
|
temperature: number;
|
|
|
|
|
windspeed: number;
|
|
|
|
|
winddirection: number;
|
|
|
|
|
weathercode: number;
|
2023-09-24 20:27:22 +07:00
|
|
|
is_day: number;
|
2023-09-24 19:55:50 +07:00
|
|
|
time: string;
|
|
|
|
|
};
|
|
|
|
|
cacheExpireAt: Date;
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2023-09-24 20:27:22 +07:00
|
|
|
|
2025-08-08 15:45:25 +07:00
|
|
|
const geoIpCacheTime = 1000 * 60 * 60 * 24;
|
|
|
|
|
const weatherCacheTime = 1000 * 60 * 15;
|
2023-09-24 19:55:50 +07:00
|
|
|
const geoIpLookupCache: GeoIpLookupCache = {};
|
|
|
|
|
const weatherLookupCache: WeatherLookupCache = {};
|
|
|
|
|
|
|
|
|
|
const isReservedIpRange = (ipAddress: string) => {
|
|
|
|
|
const ipv4Pattern =
|
|
|
|
|
/^(10\..*|172\.(1[6-9]|2[0-9]|3[0-1])\..*|192\.168\..*|127\..*|169\.254\..*|192\.88\.99\..*)$/;
|
|
|
|
|
if (ipv4Pattern.test(ipAddress)) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const ipv6LocalPattern =
|
|
|
|
|
/^fe[89ab][0-9a-fA-F]:.*|^::1$|^fc[0-9a-fA-F]{2}:.*$/;
|
|
|
|
|
if (ipv6LocalPattern.test(ipAddress.toLowerCase())) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const getGeoIp = async (ip: string) => {
|
2025-08-08 15:45:25 +07:00
|
|
|
const response = await fetch(`http://ip-api.com/json/${ip}`);
|
|
|
|
|
let data = await response.json();
|
2023-09-24 19:55:50 +07:00
|
|
|
|
|
|
|
|
data.cacheExpireAt = new Date(Date.now() + geoIpCacheTime);
|
|
|
|
|
geoIpLookupCache[ip] = data;
|
|
|
|
|
|
|
|
|
|
return data;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const getWeather = async (lat: number, lon: number) => {
|
2025-08-08 15:45:25 +07:00
|
|
|
const response = await fetch(
|
2023-09-24 19:55:50 +07:00
|
|
|
`https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lon}¤t_weather=true`
|
|
|
|
|
);
|
2025-08-08 15:45:25 +07:00
|
|
|
let data = await response.json();
|
2023-09-24 19:55:50 +07:00
|
|
|
|
|
|
|
|
data.cacheExpireAt = new Date(Date.now() + weatherCacheTime);
|
|
|
|
|
weatherLookupCache[`${lat},${lon}`] = data;
|
|
|
|
|
|
|
|
|
|
return data;
|
|
|
|
|
};
|
|
|
|
|
|
2023-09-24 20:27:22 +07:00
|
|
|
|
2025-08-08 15:45:25 +07:00
|
|
|
app.get("/", async ({ server, request, set, headers }) => {
|
|
|
|
|
let clientIp =
|
|
|
|
|
headers['cf-connecting-ip'] ||
|
|
|
|
|
headers['x-forwarded-for']?.split(',')[0]?.trim() ||
|
|
|
|
|
headers['x-real-ip'] ||
|
|
|
|
|
server?.requestIP(request)?.address;
|
2023-09-24 19:55:50 +07:00
|
|
|
|
|
|
|
|
|
2025-08-08 15:45:25 +07:00
|
|
|
if (!clientIp || clientIp === "::1" || clientIp === "127.0.0.1" || clientIp === "localhost" || clientIp === "::ffff:127.0.0.1" || isReservedIpRange(clientIp)) {
|
|
|
|
|
set.status = 400;
|
|
|
|
|
return { success: false, error: "Invalid or local IP address. External access required." };
|
2023-09-24 19:55:50 +07:00
|
|
|
}
|
|
|
|
|
|
2025-08-08 15:45:25 +07:00
|
|
|
let geoIp: typeof geoIpLookupCache[string] | undefined = geoIpLookupCache[clientIp];
|
2023-09-24 19:55:50 +07:00
|
|
|
if (geoIp) {
|
|
|
|
|
if (geoIp.cacheExpireAt < new Date()) {
|
2025-08-08 15:45:25 +07:00
|
|
|
delete geoIpLookupCache[clientIp];
|
|
|
|
|
geoIp = undefined;
|
2023-09-24 19:55:50 +07:00
|
|
|
}
|
2025-08-08 15:45:25 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!geoIp) {
|
2023-09-24 19:55:50 +07:00
|
|
|
try {
|
2025-08-08 15:45:25 +07:00
|
|
|
geoIp = await getGeoIp(clientIp);
|
2023-09-24 19:55:50 +07:00
|
|
|
} catch (e) {
|
2025-08-08 15:45:25 +07:00
|
|
|
set.status = 500;
|
|
|
|
|
return { success: false, error: "Error fetching location data" };
|
2023-09-24 19:55:50 +07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-08 15:45:25 +07:00
|
|
|
if (!geoIp) {
|
|
|
|
|
set.status = 500;
|
|
|
|
|
return { success: false, error: "Error fetching location data" };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let weather: typeof weatherLookupCache[string] | undefined = weatherLookupCache[`${geoIp.lat},${geoIp.lon}`];
|
2023-09-24 19:55:50 +07:00
|
|
|
if (weather) {
|
|
|
|
|
if (weather.cacheExpireAt < new Date()) {
|
|
|
|
|
delete weatherLookupCache[`${geoIp.lat},${geoIp.lon}`];
|
2025-08-08 15:45:25 +07:00
|
|
|
weather = undefined;
|
2023-09-24 19:55:50 +07:00
|
|
|
}
|
2025-08-08 15:45:25 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!weather) {
|
2023-09-24 19:55:50 +07:00
|
|
|
try {
|
|
|
|
|
weather = await getWeather(geoIp.lat, geoIp.lon);
|
|
|
|
|
} catch (e) {
|
2025-08-08 15:45:25 +07:00
|
|
|
set.status = 500;
|
|
|
|
|
return { success: false, error: "Error fetching weather data" };
|
2023-09-24 19:55:50 +07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-08 15:45:25 +07:00
|
|
|
if (!weather) {
|
|
|
|
|
set.status = 500;
|
|
|
|
|
return { success: false, error: "Error fetching weather data" };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
2023-09-24 20:27:22 +07:00
|
|
|
success: true,
|
|
|
|
|
data: {
|
|
|
|
|
country: geoIp.country,
|
|
|
|
|
country_code: geoIp.countryCode,
|
|
|
|
|
region: geoIp.region,
|
|
|
|
|
region_name: geoIp.regionName,
|
|
|
|
|
city: geoIp.city,
|
|
|
|
|
timezone: geoIp.timezone,
|
|
|
|
|
current_weather: {
|
|
|
|
|
temperature: weather.current_weather.temperature,
|
|
|
|
|
wind_speed: weather.current_weather.windspeed,
|
|
|
|
|
wind_direction: weather.current_weather.winddirection,
|
|
|
|
|
weather_code: weather.current_weather.weathercode,
|
|
|
|
|
is_day: weather.current_weather.is_day === 1 ? true : false,
|
|
|
|
|
time: weather.current_weather.time,
|
|
|
|
|
},
|
|
|
|
|
},
|
2025-08-08 15:45:25 +07:00
|
|
|
};
|
2023-09-24 19:55:50 +07:00
|
|
|
});
|
|
|
|
|
|
2025-08-08 15:45:25 +07:00
|
|
|
app.listen(3000, () => {
|
|
|
|
|
console.log("🦊 Elysia is running at http://localhost:3000");
|
2023-09-24 19:55:50 +07:00
|
|
|
});
|
2025-08-08 15:45:25 +07:00
|
|
|
|
|
|
|
|
export default app;
|