chore: get geo and ua info without vercel
This commit is contained in:
@@ -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);
|
||||
}
|
||||
@@ -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,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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
@@ -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",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
File diff suppressed because one or more lines are too long
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
Reference in New Issue
Block a user