diff --git a/src/typescript/frontend/src/configs/local-storage-keys.ts b/src/typescript/frontend/src/configs/local-storage-keys.ts index c34084076..f3459cca3 100644 --- a/src/typescript/frontend/src/configs/local-storage-keys.ts +++ b/src/typescript/frontend/src/configs/local-storage-keys.ts @@ -1,6 +1,7 @@ import { parseJSON, stringifyJSON } from "utils"; import packages from "../../package.json"; import { MS_IN_ONE_DAY } from "components/charts/const"; +import { satisfies, type SemVer, parse } from "semver"; const LOCAL_STORAGE_KEYS = { theme: `${packages.name}_theme`, @@ -9,7 +10,18 @@ const LOCAL_STORAGE_KEYS = { settings: `${packages.name}_settings`, }; -export const LOCAL_STORAGE_CACHE_TIME = { +const LOCAL_STORAGE_VERSIONS: { + [Property in keyof typeof LOCAL_STORAGE_KEYS]: SemVer; +} = { + theme: parse("1.0.0")!, + language: parse("1.0.0")!, + geoblocking: parse("2.0.0")!, + settings: parse("1.0.0")!, +}; + +export const LOCAL_STORAGE_CACHE_TIME: { + [Property in keyof typeof LOCAL_STORAGE_KEYS]: number; +} = { theme: Infinity, language: Infinity, geoblocking: MS_IN_ONE_DAY, @@ -19,13 +31,9 @@ export const LOCAL_STORAGE_CACHE_TIME = { export type LocalStorageCache = { expiry: number; data: T | null; + version: string | undefined; }; -/** - * Note that this data is not validated and any change in data type returned from this function - * should be validated to ensure that persisted cache data between multiple builds can cause errors - * with unexpected data types. - */ export function readLocalStorageCache(key: keyof typeof LOCAL_STORAGE_KEYS): T | null { const str = localStorage.getItem(LOCAL_STORAGE_KEYS[key]); if (str === null) { @@ -33,6 +41,16 @@ export function readLocalStorageCache(key: keyof typeof LOCAL_STORAGE_KEYS): } try { const cache = parseJSON>(str); + const range = `~${LOCAL_STORAGE_VERSIONS[key].major}`; + // Check for no breaking changes. + if (!satisfies(cache.version ?? "1.0.0", range)) { + console.warn( + `${key} cache version not satisfied (needs to satisfy ${range}, but ${cache.version} is present). Purging...` + ); + localStorage.delete(LOCAL_STORAGE_KEYS[key]); + return null; + } + // Check for staleness. if (new Date(cache.expiry) > new Date()) { return cache.data; } @@ -46,6 +64,7 @@ export function writeLocalStorageCache(key: keyof typeof LOCAL_STORAGE_KEYS, const cache: LocalStorageCache = { expiry: new Date().getTime() + LOCAL_STORAGE_CACHE_TIME[key], data, + version: LOCAL_STORAGE_VERSIONS[key].version, }; localStorage.setItem(LOCAL_STORAGE_KEYS[key], stringifyJSON>(cache)); } diff --git a/src/typescript/frontend/src/utils/geolocation.ts b/src/typescript/frontend/src/utils/geolocation.ts index 35b814630..3b96e5f0e 100644 --- a/src/typescript/frontend/src/utils/geolocation.ts +++ b/src/typescript/frontend/src/utils/geolocation.ts @@ -4,17 +4,31 @@ import { GEOBLOCKED, GEOBLOCKING_ENABLED } from "lib/server-env"; import { headers } from "next/headers"; export type Location = { - countryCode: string; - regionCode: string; + countryCode: string | null; + regionCode: string | null; }; const isDisallowedLocation = ({ countryCode, regionCode }: Location) => { - if (GEOBLOCKED.countries.includes(countryCode)) { + if (countryCode && GEOBLOCKED.countries.includes(countryCode)) { return true; } - const isoCode = `${countryCode}-${regionCode}`; - if (GEOBLOCKED.regions.includes(isoCode)) { - return true; + if (regionCode) { + const isoCode = `${countryCode}-${regionCode}`; + if (GEOBLOCKED.regions.includes(isoCode)) { + return true; + } + } + if (countryCode && !regionCode) { + if (GEOBLOCKED.regions.map((r) => r.split("-")[0]).includes(countryCode)) { + return true; + } + } + if (!countryCode && regionCode) { + // Note that even if the `regionCode` is `XX`, and `XX` is a banned country, this will return + // `true` and thus block the user, because "XX".split("-")[0] is just "XX". + if (GEOBLOCKED.countries.includes(regionCode.split("-")[0])) { + return true; + } } return false; }; @@ -23,7 +37,7 @@ export const isUserGeoblocked = async () => { if (!GEOBLOCKING_ENABLED) return false; const country = headers().get("x-vercel-ip-country"); const region = headers().get("x-vercel-ip-country-region"); - if (typeof country !== "string" || typeof region !== "string") { + if (typeof country !== "string" && typeof region !== "string") { return true; } return isDisallowedLocation({