chore: get geo and ua info without vercel

This commit is contained in:
oiov
2025-05-26 17:40:01 +08:00
parent fa02ca000b
commit cbeba449ef
16 changed files with 2644 additions and 251 deletions
-83
View File
@@ -1,83 +0,0 @@
"use server";
import { redirect } from "next/navigation";
import { siteConfig } from "@/config/site";
import { createUserShortUrlMeta, getUrlBySuffix } from "@/lib/dto/short-urls";
const redirectMap = {
"Missing[0000]": "/docs/short-urls#missing-links",
"Expired[0001]": "/docs/short-urls#expired-links",
"Disabled[0002]": "/docs/short-urls#disabled-links",
"Error[0003]": "/docs/short-urls#error-links",
"PasswordRequired[0004]": "/password-prompt?error=0&slug=",
"IncorrectPassword[0005]": "/password-prompt?error=1&slug=",
};
export async function RedirectsTo(data: any) {
const {
slug,
referer,
ip,
city,
region,
country,
latitude,
longitude,
lang,
device,
browser,
password,
} = data;
if (!slug || !ip)
return redirect(`${siteConfig.url}${redirectMap["Missing[0000]"]}${slug}`);
const res = await getUrlBySuffix(slug);
if (!res)
return redirect(`${siteConfig.url}${redirectMap["Disabled[0002]"]}${slug}`);
if (res.active !== 1)
return redirect(`${siteConfig.url}${redirectMap["Disabled[0002]"]}${slug}`);
if (res.password !== "") {
if (!password) {
return redirect(
`${siteConfig.url}${redirectMap["PasswordRequired[0004]"]}${slug}`,
);
}
if (password !== res.password) {
return redirect(
`${siteConfig.url}${redirectMap["IncorrectPassword[0005]"]}${slug}`,
);
}
}
const now = Date.now();
const createdAt = new Date(res.updatedAt).getTime();
const expirationMilliseconds = Number(res.expiration) * 1000;
const expirationTime = createdAt + expirationMilliseconds;
if (res.expiration !== "-1" && now > expirationTime) {
return redirect(`${siteConfig.url}${redirectMap["Expired[0001]"]}`);
}
await createUserShortUrlMeta({
urlId: res.id,
click: 1,
ip,
city,
region,
country,
latitude,
longitude,
referer,
lang,
device,
browser,
});
console.log("到这了");
redirect(res.target);
}
+3 -6
View File
@@ -1,14 +1,11 @@
import { ipAddress } from "@vercel/functions";
import { getIpInfo } from "@/lib/utils";
import { getIpInfo } from "@/lib/geo";
export async function GET(req: Request) {
try {
const data = getIpInfo(req);
const ip = ipAddress(req);
const data = await getIpInfo(req);
return Response.json({
ip,
ip: data.ip,
city: data.city,
region: data.region,
country: data.country,
+3 -2
View File
@@ -3,7 +3,8 @@ import TurndownService from "turndown";
import { checkApiKey } from "@/lib/dto/api-key";
import { createScrapeMeta } from "@/lib/dto/scrape";
import { getIpInfo, isLink } from "@/lib/utils";
import { getIpInfo } from "@/lib/geo";
import { isLink } from "@/lib/utils";
export const revalidate = 600;
export const dynamic = "force-dynamic";
@@ -70,7 +71,7 @@ export async function GET(req: Request) {
const markdown = turndownService.turndown(mainContent || "");
const stats = getIpInfo(req);
const stats = await getIpInfo(req);
await createScrapeMeta({
ip: stats.ip,
type: "markdown",
+3 -2
View File
@@ -2,7 +2,8 @@ import cheerio from "cheerio";
import { checkApiKey } from "@/lib/dto/api-key";
import { createScrapeMeta } from "@/lib/dto/scrape";
import { getIpInfo, isLink, removeUrlSuffix } from "@/lib/utils";
import { getIpInfo } from "@/lib/geo";
import { isLink, removeUrlSuffix } from "@/lib/utils";
export const revalidate = 600;
export const dynamic = "force-dynamic";
@@ -84,7 +85,7 @@ export async function GET(req: Request) {
$("meta[name='author']").attr("content") ||
$("meta[property='author']").attr("content");
const stats = getIpInfo(req);
const stats = await getIpInfo(req);
await createScrapeMeta({
ip: stats.ip,
type: "meta-info",
+3 -2
View File
@@ -2,9 +2,10 @@ import { ImageResponse } from "@vercel/og";
import { checkApiKey } from "@/lib/dto/api-key";
import { createScrapeMeta } from "@/lib/dto/scrape";
import { getIpInfo } from "@/lib/geo";
import { WRDO_QR_LOGO } from "@/lib/qr/constants";
import { QRCodeSVG } from "@/lib/qr/utils";
import { getIpInfo, getSearchParams } from "@/lib/utils";
import { getSearchParams } from "@/lib/utils";
import { getQRCodeQuerySchema } from "@/lib/validations/qr";
const CORS_HEADERS = {
@@ -41,7 +42,7 @@ export async function GET(req: Request) {
);
}
const stats = getIpInfo(req);
const stats = await getIpInfo(req);
await createScrapeMeta({
ip: stats.ip,
type: "qrcode",
+3 -2
View File
@@ -1,7 +1,8 @@
import { env } from "@/env.mjs";
import { checkApiKey } from "@/lib/dto/api-key";
import { createScrapeMeta } from "@/lib/dto/scrape";
import { getIpInfo, isLink } from "@/lib/utils";
import { getIpInfo } from "@/lib/geo";
import { isLink } from "@/lib/utils";
export const revalidate = 60;
@@ -69,7 +70,7 @@ export async function GET(req: Request) {
);
}
const stats = getIpInfo(req);
const stats = await getIpInfo(req);
await createScrapeMeta({
ip: stats.ip,
type: "screenshot",
+3 -2
View File
@@ -2,7 +2,8 @@ import cheerio from "cheerio";
import { checkApiKey } from "@/lib/dto/api-key";
import { createScrapeMeta } from "@/lib/dto/scrape";
import { getIpInfo, isLink } from "@/lib/utils";
import { getIpInfo } from "@/lib/geo";
import { isLink } from "@/lib/utils";
export const revalidate = 600;
export const dynamic = "force-dynamic";
@@ -63,7 +64,7 @@ export async function GET(req: Request) {
$("style").remove();
const text = $("body").text().trim();
const stats = getIpInfo(req);
const stats = await getIpInfo(req);
await createScrapeMeta({
ip: stats.ip,
type: "text",
-104
View File
@@ -1,104 +0,0 @@
import { headers } from "next/headers";
import { RedirectsTo } from "@/actions/redirect";
import UAParser from "ua-parser-js";
import { siteConfig } from "@/config/site";
import { extractRealIP, getClientGeolocation } from "@/lib/geo";
interface PageProps {
params: {
slug: string;
};
searchParams: {
password?: string;
};
}
export default async function ShortUrlPage({
params,
searchParams,
}: PageProps) {
const { slug } = params;
const { password = "" } = searchParams;
try {
const headersList = headers();
const userAgent = headersList.get("user-agent") || "";
const referer = headersList.get("referer") || "(None)";
const acceptLanguage = headersList.get("accept-language");
const realIP = extractRealIP(headersList);
const parser = new UAParser(userAgent);
const browser = parser.getBrowser();
const device = parser.getDevice();
const geo = await getClientGeolocation();
const trackingData = {
slug,
referer,
ip: realIP,
city: geo?.city,
region: geo?.region,
country: geo?.country,
latitude: geo?.latitude,
longitude: geo?.longitude,
// flag: geo?.flag,
lang: acceptLanguage?.split(",")[0],
device: device.model || "Unknown",
browser: browser.name || "Unknown",
password,
};
// await RedirectsTo(trackingData);
// await fetch(`${siteConfig.url}/api/s`, {
// method: "POST",
// // headers: { "Content-Type": "application/json" },
// body: JSON.stringify(trackingData),
// });
// const data = await getUrlBySuffix(slug);
// if (!data || data.active !== 1) redirect(redirectMap["Disabled[0002]"]);
// if (data.password !== "") {
// if (!password) {
// redirect(redirectMap["PasswordRequired[0004]"]);
// }
// if (password !== data.password) {
// redirect(redirectMap["IncorrectPassword[0005]"]);
// }
// }
// const now = Date.now();
// const createdAt = new Date(data.updatedAt).getTime();
// const expirationMilliseconds = Number(data.expiration) * 1000;
// const expirationTime = createdAt + expirationMilliseconds;
// if (data.expiration !== "-1" && now > expirationTime) {
// redirect(redirectMap["Expired[0001]"]);
// }
// await createUserShortUrlMeta({
// urlId: data.id,
// click: 1,
// ip: realIP,
// city: geo?.city || "",
// region: geo?.region || "",
// country: geo?.country || "",
// latitude: geo?.latitude || "",
// longitude: geo?.longitude || "",
// referer,
// lang: acceptLanguage?.split(",")[0] || "",
// device: device.model || "Unknown",
// browser: browser.name || "Unknown",
// });
// 重定向到目标URL
// window.location.href = data.target;
// redirect(data.target);
} catch (error) {
console.error("Short URL redirect error:", error);
}
}
+56 -12
View File
@@ -1,17 +1,37 @@
interface GeoData {
ip: string;
ipVersion?: string;
country: string;
city: string;
region: string;
timezone?: string;
isp?: string;
asn?: number;
latitude: string;
longitude: string;
import { userAgent } from "next/server";
import { Geo, geolocation } from "@vercel/functions";
import { NextAuthRequest } from "next-auth/lib";
import UAParser from "ua-parser-js";
const isVercel = process.env.VERCEL;
export async function getGeolocation(req: NextAuthRequest) {
if (isVercel) {
return geolocation(req);
} else {
return await getClientGeolocation();
}
}
export async function getClientGeolocation(): Promise<GeoData | null> {
export function getUserAgent(req: NextAuthRequest) {
if (isVercel) {
return userAgent(req);
} else {
const headers = req.headers;
const userAgent = headers.get("user-agent") || "";
const parser = new UAParser(userAgent);
return {
browser: parser.getBrowser(),
device: parser.getDevice(),
os: parser.getOS(),
engine: parser.getEngine(),
cpu: parser.getCPU(),
isBot: false,
};
}
}
export async function getClientGeolocation(): Promise<Geo | null> {
const response = await fetch("https://ip.wr.do/api", {
signal: AbortSignal.timeout(3000),
});
@@ -52,3 +72,27 @@ function isValidIP(ip: string): boolean {
return ipv4Regex.test(ip) || ipv6Regex.test(ip);
}
export async function getIpInfo(req) {
const headers = req.headers;
const ip = extractRealIP(headers);
const geo = await getGeolocation(req);
const ua = getUserAgent(req);
const userLanguage =
req.headers.get("accept-language")?.split(",")[0] || "en-US";
return {
referer: headers.get("referer") || "(None)",
ip,
city: geo?.city || "",
region: geo?.region || "",
country: geo?.country || "",
latitude: geo?.latitude || "",
longitude: geo?.longitude || "",
flag: geo?.flag,
lang: userLanguage,
device: ua.device.model || "Unknown",
browser: ua.browser.name || "Unknown",
};
}
-30
View File
@@ -1,12 +1,9 @@
import crypto from "crypto";
import { Metadata } from "next";
import { geolocation } from "@vercel/functions";
import { clsx, type ClassValue } from "clsx";
import ms from "ms";
import { twMerge } from "tailwind-merge";
import UAParser from "ua-parser-js";
import { env } from "@/env.mjs";
import { siteConfig } from "@/config/site";
import { TIME_RANGES } from "./enums";
@@ -248,33 +245,6 @@ export function removeUrlSuffix(url: string): string {
return url.startsWith("http") ? url.split("//")[1] : url;
}
export function getIpInfo(req: Request) {
const geo = geolocation(req);
const ua = req.headers.get("user-agent") || "";
const parser = new UAParser();
parser.setUA(ua);
const browser = parser.getBrowser();
const device = parser.getDevice();
const referer = req.headers.get("referer") || "(None)";
const ip = req.headers.get("X-Forwarded-For") || "127.0.0.1";
const userLanguage =
req.headers.get("accept-language")?.split(",")[0] || "en-US";
return {
referer,
ip,
city: geo?.city || "",
region: geo?.region || "",
country: geo?.country || "",
latitude: geo?.latitude || "",
longitude: geo?.longitude || "",
flag: geo?.flag,
lang: userLanguage,
device: device.model || "Unknown",
browser: browser.name || "Unknown",
};
}
export function toCamelCase(str: string) {
return str
.split("-")
+11 -4
View File
@@ -2,8 +2,15 @@ import { NextResponse, userAgent } from "next/server";
import { geolocation } from "@vercel/functions";
import { auth } from "auth";
import { NextAuthRequest } from "next-auth/lib";
import UAParser from "ua-parser-js";
import { siteConfig } from "./config/site";
import {
extractRealIP,
getClientGeolocation,
getGeolocation,
getUserAgent,
} from "./lib/geo";
export const config = {
matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
@@ -25,9 +32,10 @@ async function handleShortUrl(req: NextAuthRequest) {
if (!slug)
return NextResponse.redirect(`${siteConfig.url}/docs/short-urls`, 302);
const geo = geolocation(req);
const headers = req.headers;
const ua = userAgent(req);
const ip = extractRealIP(headers);
const geo = await getGeolocation(req);
const ua = getUserAgent(req);
const url = new URL(req.url);
const password = url.searchParams.get("password") || "";
@@ -35,7 +43,7 @@ async function handleShortUrl(req: NextAuthRequest) {
const trackingData = {
slug,
referer: headers.get("referer") || "(None)",
ip: headers.get("X-Forwarded-For"),
ip,
city: geo?.city,
region: geo?.region,
country: geo?.country,
@@ -94,7 +102,6 @@ async function handleShortUrl(req: NextAuthRequest) {
return NextResponse.redirect(target, 302);
}
// 提取 slug
function extractSlug(url: string): string | null {
const match = url.match(/([^/?]+)(?:\?.*)?$/);
return match ? match[1] : null;
+101 -1
View File
File diff suppressed because one or more lines are too long
+1
View File
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long