Browse Source

refactor: improve geo-location handling by standardizing country name resolution and forwarding client IP headers to the backend

master
mortezaei 4 days ago
parent
commit
73758ec53f
  1. 27
      src/app/api/proxy/route.ts
  2. 43
      src/components/Componentes/question-birthplace.tsx
  3. 49
      src/data/countries.ts
  4. 14
      src/lib/geo-region.ts

27
src/app/api/proxy/route.ts

@ -17,6 +17,14 @@ const REQUEST_HEADERS_TO_FORWARD = [
"x-csrftoken",
"x-requested-with",
"x-xsrf-token",
"cf-connecting-ip",
"cf-ipcountry",
"cf-ray",
"cf-visitor",
"x-real-ip",
"x-forwarded-for",
"x-forwarded-proto",
"x-forwarded-host",
];
const RESPONSE_HEADERS_TO_DROP = [
@ -166,6 +174,25 @@ function getRequestHeaders(request: NextRequest, targetUrl: URL) {
headers.set("referer", `${targetUrl.origin}/`);
headers.set("user-agent", "dart:io");
// Forward real client IP and Geo headers to Django backend
const clientIp =
request.headers.get("cf-connecting-ip") ||
request.headers.get("x-real-ip") ||
request.headers.get("x-forwarded-for")?.split(",")[0]?.trim();
if (clientIp) {
headers.set("cf-connecting-ip", clientIp);
headers.set("x-real-ip", clientIp);
if (!headers.has("x-forwarded-for")) {
headers.set("x-forwarded-for", clientIp);
}
}
const cfCountry = request.headers.get("cf-ipcountry");
if (cfCountry) {
headers.set("cf-ipcountry", cfCountry);
}
return headers;
}

43
src/components/Componentes/question-birthplace.tsx

@ -2,7 +2,7 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { createPortal } from "react-dom";
import { getCountryList, COUNTRIES_EN, COUNTRIES_FA } from "@/data/countries";
import { getCountryList, resolveCountryName, COUNTRIES_EN, COUNTRIES_FA } from "@/data/countries";
import type { QuestionField } from "@/lib/schema-adapter";
import { useI18n } from "@/translations/provider";
import { useQuestionAnswers } from "./question-answer-storage";
@ -93,18 +93,21 @@ export function QuestionBirthplace({
const storedRegion = isResidence ? getStoredUserGeoRegion() : null;
const initial = parseValue(rawValue);
const localizedInitialCountry = resolveCountryName(
initial.country || storedRegion?.country,
locale,
);
const initialCity = initial.city || storedRegion?.city || "";
const initialLoc =
initial.country || initial.city
? [initial.city, initial.country].filter(Boolean).join(", ")
: storedRegion?.city || storedRegion?.country
? [storedRegion.city, storedRegion.country].filter(Boolean).join(", ")
localizedInitialCountry || initialCity
? [initialCity, localizedInitialCountry].filter(Boolean).join(", ")
: "";
const [selectedCountry, setSelectedCountry] = useState(
() => initial.country || storedRegion?.country || "",
() => localizedInitialCountry || "",
);
const [cityInput, setCityInput] = useState(
() => initial.city || storedRegion?.city || "",
() => initialCity,
);
const [isOpen, setIsOpen] = useState(false);
@ -170,8 +173,11 @@ export function QuestionBirthplace({
// If there is already a saved answer and we are not forcing, display it
if (rawValue && !force) {
const parsed = parseValue(rawValue);
if (parsed.country || parsed.city) {
const loc = [parsed.city, parsed.country].filter(Boolean).join(", ");
const cName = resolveCountryName(parsed.country, locale);
if (cName || parsed.city) {
const loc = [parsed.city, cName].filter(Boolean).join(", ");
setSelectedCountry(cName);
setCityInput(parsed.city);
setDetectedLocation(loc);
setMode("auto");
return;
@ -185,7 +191,8 @@ export function QuestionBirthplace({
if (!isMountedRef.current) return;
const city = region.city || "";
const country = region.country || "";
const rawCountry = region.country || region.countryCode || "";
const country = resolveCountryName(rawCountry, locale) || rawCountry;
if (city || country) {
const loc = [city, country].filter(Boolean).join(", ");
@ -206,7 +213,7 @@ export function QuestionBirthplace({
setIsDetecting(false);
}
}
}, [rawValue]);
}, [rawValue, locale]);
useEffect(() => {
if (isLoading) return;
@ -226,7 +233,8 @@ export function QuestionBirthplace({
}
setMode("manual");
const parsed = parseValue(rawValue);
const country = selectedCountry || parsed.country;
const resolvedC = resolveCountryName(selectedCountry || parsed.country, locale);
const country = resolvedC || selectedCountry || parsed.country;
const city = cityInput || parsed.city;
setSelectedCountry(country);
setCityInput(city);
@ -236,16 +244,17 @@ export function QuestionBirthplace({
// Synchronize state if rawValue changes externally
useEffect(() => {
const updated = parseValue(rawValue);
if (updated.country !== selectedCountry) {
setSelectedCountry(updated.country);
const resolvedC = resolveCountryName(updated.country, locale);
if (resolvedC && resolvedC !== selectedCountry) {
setSelectedCountry(resolvedC);
}
if (updated.city.trim() !== cityInput.trim()) {
setCityInput(updated.city);
}
if (updated.country && updated.city) {
setDetectedLocation(`${updated.city}, ${updated.country}`);
if (resolvedC || updated.city) {
setDetectedLocation([updated.city, resolvedC].filter(Boolean).join(", "));
}
}, [rawValue]);
}, [rawValue, locale]);
const options = getCountryList(locale);
const filteredOptions = options.filter((option) =>

49
src/data/countries.ts

@ -399,3 +399,52 @@ export function getCountryList(locale: string): string[] {
}
return COUNTRIES_EN;
}
export function resolveCountryName(
countryOrCode: string | undefined | null,
locale: string = "fa",
): string {
if (!countryOrCode) return "";
const trimmed = countryOrCode.trim();
if (!trimmed) return "";
const isFa = String(locale || "en").toLowerCase().startsWith("fa");
// 1. If 2-letter ISO code (e.g. "DE", "IR", "US", "TJ")
if (trimmed.length === 2 && /^[a-zA-Z]{2}$/.test(trimmed)) {
try {
const displayNames = new Intl.DisplayNames([isFa ? "fa" : "en"], {
type: "region",
});
const name = displayNames.of(trimmed.toUpperCase());
if (name) return name;
} catch {}
}
// 2. If it matches an English country name in COUNTRIES_EN
const enIndex = COUNTRIES_EN.findIndex(
(c) => c.toLowerCase() === trimmed.toLowerCase(),
);
if (enIndex !== -1) {
return isFa ? COUNTRIES_FA[enIndex] || COUNTRIES_EN[enIndex] : COUNTRIES_EN[enIndex];
}
// 3. If it matches a Persian country name in COUNTRIES_FA
const faIndex = COUNTRIES_FA.findIndex((c) => c === trimmed);
if (faIndex !== -1) {
return isFa ? COUNTRIES_FA[faIndex] : COUNTRIES_EN[faIndex] || COUNTRIES_FA[faIndex];
}
// 4. Try Intl.DisplayNames fallback if 2-3 chars
if (trimmed.length <= 3) {
try {
const displayNames = new Intl.DisplayNames([isFa ? "fa" : "en"], {
type: "region",
});
const name = displayNames.of(trimmed.toUpperCase());
if (name) return name;
} catch {}
}
return trimmed;
}

14
src/lib/geo-region.ts

@ -2,6 +2,7 @@
import { PhoneNumberUtil } from "google-libphonenumber";
import { http } from "./http";
import { resolveCountryName } from "@/data/countries";
export type UserGeoRegion = {
ip?: string;
@ -103,12 +104,19 @@ export function getUserGeoRegion(): Promise<UserGeoRegion> {
const data = response.data;
if (data && (data.country || data.country_code || data.city)) {
const phoneCode = resolvePhoneCodeFromCountryCode(data.country_code);
const isoCode =
data.country_code ||
(data.country && data.country.trim().length === 2
? data.country.trim().toUpperCase()
: undefined);
const phoneCode = resolvePhoneCodeFromCountryCode(isoCode);
const countryName =
resolveCountryName(data.country || isoCode, "en") || data.country;
const region: UserGeoRegion = {
ip: data.ip,
city: data.city,
country: data.country,
countryCode: data.country_code,
country: countryName,
countryCode: isoCode,
phoneCode: phoneCode || "+44",
};
setStoredUserGeoRegion(region);

Loading…
Cancel
Save