Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ECO-2505] Use Vercel headers to get location #421

Merged
merged 4 commits into from
Nov 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions src/typescript/example.env
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,6 @@ ALLOWLISTER3K_URL="http://localhost:3000"
# A list of ISO 3166-2 codes of countries and regions to geoblock.
GEOBLOCKED='{"countries":[],"regions":[]}'

# The vpnapi.io API key.
# If GEOBLOCKED is set and non-empty, this needs to be set as well.
VPNAPI_IO_API_KEY=""

# The private key of the contract publisher.
# No "0x" at the start.
# Useful in testing only.
Expand Down
5 changes: 4 additions & 1 deletion src/typescript/frontend/src/app/verify_status/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import VerifyStatusPage from "components/pages/verify_status/VerifyStatusPage";
import { headers } from "next/headers";

const Verify = async () => {
return <VerifyStatusPage />;
const country = headers().get("x-vercel-ip-country");
const region = headers().get("x-vercel-ip-country-region");
return <VerifyStatusPage country={country} region={region} />;
};

export default Verify;
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,13 @@ const checkmarkOrX = (checkmark: boolean) => (
/>
);

export const ClientVerifyPage = () => {
export const ClientVerifyPage = ({
country,
region,
}: {
country: string | null;
region: string | null;
}) => {
const { account } = useAptos();
const { connected, disconnect } = useWallet();
const [galxe, setGalxe] = useState(false);
Expand Down Expand Up @@ -64,7 +70,7 @@ export const ClientVerifyPage = () => {
</motion.div>
)}
<ButtonWithConnectWalletFallback forceAllowConnect={true} arrow={false}>
<div className="flex flex-col uppercase mt-[20ch] gap-1">
<div className="flex flex-col uppercase mt-[30ch] gap-1">
<div>
Wallet address:{" "}
<span className="text-warning">
Expand All @@ -74,6 +80,8 @@ export const ClientVerifyPage = () => {
<div>Galxe: {checkmarkOrX(galxe)}</div>
<div>Custom allowlist: {checkmarkOrX(customAllowlisted)}</div>
<div>Passes geoblocking: {checkmarkOrX(!geoblocked)}</div>
<div>Country: {country ?? "unknown"}</div>
<div>Region: {region ?? "unknown"}</div>
<a
className="underline text-ec-blue"
href={process.env.NEXT_PUBLIC_GALXE_CAMPAIGN_REDIRECT}
Expand Down
5 changes: 3 additions & 2 deletions src/typescript/frontend/src/configs/local-storage-keys.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { parseJSON, stringifyJSON } from "utils";
import packages from "../../package.json";
import { MS_IN_ONE_DAY } from "components/charts/const";

const LOCAL_STORAGE_KEYS = {
theme: `${packages.name}_theme`,
Expand All @@ -8,10 +9,10 @@ const LOCAL_STORAGE_KEYS = {
settings: `${packages.name}_settings`,
};

const LOCAL_STORAGE_CACHE_TIME = {
export const LOCAL_STORAGE_CACHE_TIME = {
theme: Infinity,
language: Infinity,
geoblocking: 7 * 24 * 60 * 60 * 1000, // 7 days.
geoblocking: MS_IN_ONE_DAY,
settings: Infinity,
};

Expand Down
11 changes: 7 additions & 4 deletions src/typescript/frontend/src/hooks/use-is-user-geoblocked.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { useQuery } from "@tanstack/react-query";
import { MS_IN_ONE_DAY } from "components/charts/const";
import { readLocalStorageCache, writeLocalStorageCache } from "configs/local-storage-keys";
import {
LOCAL_STORAGE_CACHE_TIME,
readLocalStorageCache,
writeLocalStorageCache,
} from "configs/local-storage-keys";
import { isUserGeoblocked } from "utils/geolocation";

const SEVEN_DAYS_MS = 7 * MS_IN_ONE_DAY;
const FETCH_IS_GEOBLOCKED_CACHE_TIME = LOCAL_STORAGE_CACHE_TIME["geoblocking"];

const useIsUserGeoblocked = (args?: { explicitlyGeoblocked: boolean }) => {
// In some cases we may want to know if the query's return value is explicitly true.
Expand All @@ -20,7 +23,7 @@ const useIsUserGeoblocked = (args?: { explicitlyGeoblocked: boolean }) => {

return geoblocked;
},
staleTime: SEVEN_DAYS_MS,
staleTime: FETCH_IS_GEOBLOCKED_CACHE_TIME,
placeholderData: (prev) => prev,
});

Expand Down
9 changes: 0 additions & 9 deletions src/typescript/frontend/src/lib/server-env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,8 @@ export const GEOBLOCKED: { countries: string[]; regions: string[] } = JSON.parse
);
export const GEOBLOCKING_ENABLED = GEOBLOCKED.countries.length > 0 || GEOBLOCKED.regions.length > 0;

if (GEOBLOCKING_ENABLED) {
if (process.env.VPNAPI_IO_API_KEY === "undefined") {
throw new Error(
"Geoblocking is enabled but environment variable VPNAPI_IO_API_KEY is undefined."
);
}
}

export const ALLOWLISTER3K_URL: string | undefined = process.env.ALLOWLISTER3K_URL;
export const REVALIDATION_TIME: number = Number(process.env.REVALIDATION_TIME);
export const VPNAPI_IO_API_KEY: string = process.env.VPNAPI_IO_API_KEY!;
export const PRE_LAUNCH_TEASER: boolean = process.env.PRE_LAUNCH_TEASER === "true";

if (
Expand Down
51 changes: 11 additions & 40 deletions src/typescript/frontend/src/utils/geolocation.ts
Original file line number Diff line number Diff line change
@@ -1,62 +1,33 @@
"use server";

import { GEOBLOCKED, GEOBLOCKING_ENABLED, VPNAPI_IO_API_KEY } from "lib/server-env";
import { GEOBLOCKED, GEOBLOCKING_ENABLED } from "lib/server-env";
import { headers } from "next/headers";

export type Location = {
country: string;
region: string;
countryCode: string;
regionCode: string;
vpn: boolean;
};

const isDisallowedLocation = (location: Location) => {
if (GEOBLOCKED.countries.includes(location.countryCode)) {
const isDisallowedLocation = ({ countryCode, regionCode }: Location) => {
if (GEOBLOCKED.countries.includes(countryCode)) {
return true;
}
const isoCode = `${location.countryCode}-${location.regionCode}`;
const isoCode = `${countryCode}-${regionCode}`;
if (GEOBLOCKED.regions.includes(isoCode)) {
return true;
}
return false;
};

export const isUserGeoblocked = async () => {
const ip = headers().get("x-real-ip");
if (!GEOBLOCKING_ENABLED) return false;
if (ip === "undefined" || typeof ip === "undefined" || ip === "null" || ip === null) {
const country = headers().get("x-vercel-ip-country");
const region = headers().get("x-vercel-ip-country-region");
if (typeof country !== "string" || typeof region !== "string") {
return true;
}
let location: Location;
try {
location = await getLocation(ip);
} catch (_) {
return true;
}
if (location.vpn) {
return true;
}
return isDisallowedLocation(location);
};

const ONE_DAY = 604800;

const getLocation: (ip: string) => Promise<Location> = async (ip) => {
if (ip === "undefined" || typeof ip === "undefined") {
throw new Error("IP is undefined");
}
const queryResult = await fetch(`https://vpnapi.io/api/${ip}?key=${VPNAPI_IO_API_KEY}`, {
next: { revalidate: ONE_DAY },
}).then((res) => res.json());

const data = {
country: queryResult.location.country,
region: queryResult.location.region,
countryCode: queryResult.location.country_code,
regionCode: queryResult.location.region_code,
vpn: queryResult.security.vpn,
};

return data;
return isDisallowedLocation({
countryCode: country,
regionCode: region,
});
};
Loading