Skip to content

Commit bde746b

Browse files
committed
feat: Enhance FingerprintEditor with country search functionality and dropdown
1 parent 771dfa8 commit bde746b

3 files changed

Lines changed: 314 additions & 34 deletions

File tree

backend/api/routes/fingerprint_hub.py

Lines changed: 25 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -282,41 +282,45 @@ async def randomize_fingerprint_hub(
282282
# Preserve or use device_model
283283
model = device_model or fp.device_model or "SM-G996B"
284284

285+
# Preserve existing values that are important
286+
build_id = fp.build_id or "AP3A.240905.015.A2"
287+
ap_version = fp.ap_version or "G996BXXSJHZC2"
288+
android_version = fp.android_version or "15"
289+
manufacturer = fp.manufacturer or "samsung"
290+
brand = fp.brand or "samsung"
291+
device_codename = fp.device_codename or "o1s"
292+
285293
# Generate realistic identifiers
286294
new_data = {
287295
"device_model": model,
288296
"imei": generate_imei(model),
289297
"android_id": generate_android_id(),
290298
"mac_address": random_samsung_mac(),
291299
"serial_number": random_samsung_serial(model),
292-
"latitude": location["latitude"],
293-
"longitude": location["longitude"],
294-
"altitude": location["altitude_m"],
300+
"latitude": location.get("latitude", 0.0),
301+
"longitude": location.get("longitude", 0.0),
302+
"altitude": location.get("altitude", 0.0),
295303
"timezone": location.get("timezone", "UTC"),
296-
"network_type": "LTE", # default to LTE
304+
"network_type": "LTE",
297305
"ip_address": ip,
298306
"mcc": carrier.get("mcc"),
299307
"mnc": carrier.get("mnc"),
308+
"manufacturer": manufacturer,
309+
"brand": brand,
310+
"device_codename": device_codename,
311+
"build_fingerprint": build_fingerprint_for_model(
312+
model, ap_version, android_version, build_id
313+
),
300314
}
301315

302-
# Build fingerprint string if we have build_id, ap_version, android_version
303-
if fp.build_id and fp.ap_version and fp.android_version:
304-
new_data["build_fingerprint"] = build_fingerprint_for_model(
305-
model, fp.ap_version, fp.android_version, fp.build_id
306-
)
307-
308-
# Keep manufacturer/brand/etc. from existing profile if not set
309-
if not fp.manufacturer:
310-
new_data["manufacturer"] = "samsung"
311-
if not fp.brand:
312-
new_data["brand"] = "samsung"
313-
if not fp.device_codename:
314-
new_data["device_codename"] = "o1s"
315-
316-
# Apply to ORM row
316+
# Apply to ORM row safely
317317
for k, v in new_data.items():
318-
if hasattr(fp, k) and v is not None:
319-
setattr(fp, k, v)
318+
if v is not None:
319+
try:
320+
setattr(fp, k, v)
321+
except (AttributeError, TypeError):
322+
# Skip fields that don't exist or can't be set
323+
pass
320324

321325
# Handle extended JSON (preserve existing or empty)
322326
if fp.extended_json:

backend/core/fingerprint/geo_database.py

Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,239 @@
393393
"language": "id",
394394
"country_iso": "id",
395395
},
396+
# ═══════════════════════════════════════════════════════════════════════════
397+
# شرق أوسط / Middle East
398+
# ═══════════════════════════════════════════════════════════════════════════
399+
"IQ": {
400+
"name": "Iraq",
401+
"cities": [
402+
("Baghdad", 33.3128, 44.3615, 34),
403+
("Basra", 30.5085, 47.8078, 3),
404+
("Mosul", 36.3538, 43.1581, 225),
405+
("Erbil", 36.1921, 43.9889, 929),
406+
],
407+
"carriers": [
408+
{"name": "Zain IQ", "mcc": "418", "mnc": "05", "apn": "zain"},
409+
{"name": "Asiacell", "mcc": "418", "mnc": "08", "apn": "asiacell"},
410+
{"name": "Ooredoo IQ", "mcc": "418", "mnc": "40", "apn": "ooredoo"},
411+
],
412+
"ip_prefixes": ["37.128.", "94.101.", "185.37.", "185.97."],
413+
"timezone": "Asia/Baghdad",
414+
"language": "ar",
415+
"country_iso": "iq",
416+
},
417+
"KW": {
418+
"name": "Kuwait",
419+
"cities": [
420+
("Kuwait City", 29.3759, 47.9774, 10),
421+
("Salmiya", 29.3809, 47.8289, 8),
422+
],
423+
"carriers": [
424+
{"name": "Zain KW", "mcc": "419", "mnc": "02", "apn": "zain.net"},
425+
{"name": "Ooredoo KW", "mcc": "419", "mnc": "03", "apn": "internet"},
426+
{"name": "VIVA", "mcc": "419", "mnc": "04", "apn": "viva.net"},
427+
],
428+
"ip_prefixes": ["37.22.", "82.148.", "94.101.", "195.229."],
429+
"timezone": "Asia/Kuwait",
430+
"language": "ar",
431+
"country_iso": "kw",
432+
},
433+
"JO": {
434+
"name": "Jordan",
435+
"cities": [
436+
("Amman", 31.9454, 35.9284, 777),
437+
("Zarqa", 32.0551, 36.0876, 526),
438+
("Irbid", 32.5537, 35.8501, 560),
439+
],
440+
"carriers": [
441+
{"name": "Zain JO", "mcc": "416", "mnc": "01", "apn": "internet"},
442+
{"name": "Orange JO", "mcc": "416", "mnc": "02", "apn": "orange"},
443+
{"name": "Umniah", "mcc": "416", "mnc": "10", "apn": "umniah"},
444+
],
445+
"ip_prefixes": ["82.102.", "94.201.", "196.1.", "213.6."],
446+
"timezone": "Asia/Amman",
447+
"language": "ar",
448+
"country_iso": "jo",
449+
},
450+
"LB": {
451+
"name": "Lebanon",
452+
"cities": [
453+
("Beirut", 33.8886, 35.4955, 45),
454+
("Tripoli", 34.4347, 35.8449, 20),
455+
("Sidon", 33.5644, 35.3756, 10),
456+
],
457+
"carriers": [
458+
{"name": "Alfa", "mcc": "415", "mnc": "01", "apn": "alfa"},
459+
{"name": "TouchTel", "mcc": "415", "mnc": "03", "apn": "touchtel.net"},
460+
{"name": "Ogero", "mcc": "415", "mnc": "05", "apn": "internet"},
461+
],
462+
"ip_prefixes": ["37.131.", "80.84.", "195.154.", "217.115."],
463+
"timezone": "Asia/Beirut",
464+
"language": "ar",
465+
"country_iso": "lb",
466+
},
467+
"BH": {
468+
"name": "Bahrain",
469+
"cities": [
470+
("Manama", 26.2235, 50.5861, 10),
471+
],
472+
"carriers": [
473+
{"name": "Zain BH", "mcc": "426", "mnc": "01", "apn": "internet"},
474+
{"name": "Ooredoo BH", "mcc": "426", "mnc": "02", "apn": "internet"},
475+
{"name": "VIVA", "mcc": "426", "mnc": "04", "apn": "viva"},
476+
],
477+
"ip_prefixes": ["82.148.", "94.200.", "185.3."],
478+
"timezone": "Asia/Bahrain",
479+
"language": "ar",
480+
"country_iso": "bh",
481+
},
482+
"OM": {
483+
"name": "Oman",
484+
"cities": [
485+
("Muscat", 23.6144, 58.5922, 10),
486+
("Salalah", 17.0151, 54.0924, 26),
487+
("Nizwa", 22.9341, 57.5411, 618),
488+
],
489+
"carriers": [
490+
{"name": "Oman Mobile", "mcc": "422", "mnc": "02", "apn": "internet"},
491+
{"name": "Ooredoo OM", "mcc": "422", "mnc": "01", "apn": "internet"},
492+
],
493+
"ip_prefixes": ["82.102.", "185.194.", "212.27."],
494+
"timezone": "Asia/Muscat",
495+
"language": "ar",
496+
"country_iso": "om",
497+
},
498+
"QA": {
499+
"name": "Qatar",
500+
"cities": [
501+
("Doha", 25.2854, 51.5310, 10),
502+
],
503+
"carriers": [
504+
{"name": "Ooredoo QA", "mcc": "427", "mnc": "01", "apn": "internet"},
505+
{"name": "Vodafone QA", "mcc": "427", "mnc": "02", "apn": "internet"},
506+
],
507+
"ip_prefixes": ["82.148.", "185.3.", "195.88."],
508+
"timezone": "Asia/Qatar",
509+
"language": "ar",
510+
"country_iso": "qa",
511+
},
512+
"PS": {
513+
"name": "Palestine",
514+
"cities": [
515+
("Gaza", 31.5167, 34.4667, 10),
516+
("Ramallah", 31.9454, 35.1932, 860),
517+
("Bethlehem", 31.9454, 35.1932, 765),
518+
],
519+
"carriers": [
520+
{"name": "Jawwal", "mcc": "425", "mnc": "01", "apn": "www"},
521+
{"name": "Ooredoo PS", "mcc": "425", "mnc": "02", "apn": "3gnet"},
522+
{"name": "Wataniya", "mcc": "425", "mnc": "03", "apn": "wataniya"},
523+
],
524+
"ip_prefixes": ["89.163.", "185.180.", "195.88."],
525+
"timezone": "Asia/Gaza",
526+
"language": "ar",
527+
"country_iso": "ps",
528+
},
529+
"SY": {
530+
"name": "Syria",
531+
"cities": [
532+
("Damascus", 33.5138, 36.2765, 680),
533+
("Aleppo", 36.2021, 37.1671, 380),
534+
("Homs", 34.7302, 36.7232, 300),
535+
],
536+
"carriers": [
537+
{"name": "Syriatel", "mcc": "963", "mnc": "01", "apn": "internet"},
538+
{"name": "MTN SY", "mcc": "963", "mnc": "20", "apn": "internet"},
539+
],
540+
"ip_prefixes": ["37.67.", "82.103.", "195.154."],
541+
"timezone": "Asia/Damascus",
542+
"language": "ar",
543+
"country_iso": "sy",
544+
},
545+
"YE": {
546+
"name": "Yemen",
547+
"cities": [
548+
("Sana'a", 15.3694, 48.2219, 2270),
549+
("Aden", 12.7751, 45.3569, 50),
550+
("Taiz", 13.5769, 44.0210, 1404),
551+
],
552+
"carriers": [
553+
{"name": "Sabafon", "mcc": "421", "mnc": "01", "apn": "internet"},
554+
{"name": "MTN YE", "mcc": "421", "mnc": "03", "apn": "mtn"},
555+
{"name": "Yemen Mobile", "mcc": "421", "mnc": "02", "apn": "internet"},
556+
],
557+
"ip_prefixes": ["81.201.", "156.96.", "196.3."],
558+
"timezone": "Asia/Aden",
559+
"language": "ar",
560+
"country_iso": "ye",
561+
},
562+
"LY": {
563+
"name": "Libya",
564+
"cities": [
565+
("Tripoli", 32.8872, 13.1913, 18),
566+
("Benghazi", 32.1165, 20.0582, 10),
567+
("Misrata", 32.3741, 15.0930, 10),
568+
],
569+
"carriers": [
570+
{"name": "Libyana", "mcc": "606", "mnc": "01", "apn": "internet"},
571+
{"name": "Almadar", "mcc": "606", "mnc": "06", "apn": "internet"},
572+
],
573+
"ip_prefixes": ["41.230.", "196.1.", "212.94."],
574+
"timezone": "Africa/Tripoli",
575+
"language": "ar",
576+
"country_iso": "ly",
577+
},
578+
"DZ": {
579+
"name": "Algeria",
580+
"cities": [
581+
("Algiers", 36.7538, 3.0588, 73),
582+
("Oran", 35.6969, -0.6308, 90),
583+
("Constantine", 36.3575, 6.6145, 644),
584+
],
585+
"carriers": [
586+
{"name": "Ooredoo DZ", "mcc": "603", "mnc": "01", "apn": "internet"},
587+
{"name": "Mobilis", "mcc": "603", "mnc": "02", "apn": "mobilis"},
588+
{"name": "Zain DZ", "mcc": "603", "mnc": "03", "apn": "internet"},
589+
],
590+
"ip_prefixes": ["41.107.", "105.186.", "196.200.", "41.228."],
591+
"timezone": "Africa/Algiers",
592+
"language": "ar",
593+
"country_iso": "dz",
594+
},
595+
"TN": {
596+
"name": "Tunisia",
597+
"cities": [
598+
("Tunis", 36.8065, 10.1686, 70),
599+
("Sfax", 34.7406, 10.7603, 15),
600+
("Sousse", 35.8256, 10.6369, 15),
601+
],
602+
"carriers": [
603+
{"name": "Tunisie Telecom", "mcc": "605", "mnc": "01", "apn": "internet"},
604+
{"name": "Ooredoo TN", "mcc": "605", "mnc": "03", "apn": "internet"},
605+
{"name": "Orange TN", "mcc": "605", "mnc": "02", "apn": "orange"},
606+
],
607+
"ip_prefixes": ["41.226.", "196.200.", "212.7."],
608+
"timezone": "Africa/Tunis",
609+
"language": "ar",
610+
"country_iso": "tn",
611+
},
612+
"SD": {
613+
"name": "Sudan",
614+
"cities": [
615+
("Khartoum", 15.5007, 32.5599, 380),
616+
("Omdurman", 15.5500, 32.4833, 380),
617+
("Port Sudan", 19.6173, 37.2169, 10),
618+
],
619+
"carriers": [
620+
{"name": "Zain SD", "mcc": "639", "mnc": "01", "apn": "internet"},
621+
{"name": "Sudani", "mcc": "639", "mnc": "02", "apn": "internet"},
622+
{"name": "MTarife", "mcc": "639", "mnc": "03", "apn": "mtarife"},
623+
],
624+
"ip_prefixes": ["41.77.", "196.1.", "196.3."],
625+
"timezone": "Africa/Khartoum",
626+
"language": "ar",
627+
"country_iso": "sd",
628+
},
396629
}
397630

398631

frontend/src/components/FingerprintEditor.tsx

Lines changed: 56 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ export default function FingerprintEditor({ deviceId, isRunning }: Props) {
3838
const [statusMsg, setStatusMsg] = useState<string | null>(null)
3939
const [applyDetail, setApplyDetail] = useState<string | null>(null)
4040
const [selectedCountry, setSelectedCountry] = useState<string>('')
41+
const [countrySearch, setCountrySearch] = useState<string>('')
42+
const [showCountryDropdown, setShowCountryDropdown] = useState(false)
4143

4244
const { data: fp, isLoading } = useQuery({
4345
queryKey: ['fingerprint', deviceId],
@@ -150,25 +152,66 @@ export default function FingerprintEditor({ deviceId, isRunning }: Props) {
150152
const countryOptions = Object.entries(countriesData?.countries ?? {})
151153
.sort(([, a], [, b]) => a.name.localeCompare(b.name))
152154

155+
// Filter countries by search term
156+
const filteredCountries = countryOptions.filter(([code, country]) =>
157+
country.name.toLowerCase().includes(countrySearch.toLowerCase()) ||
158+
code.toLowerCase().includes(countrySearch.toLowerCase())
159+
)
160+
161+
const selectedCountryName = countryOptions.find(([code]) => code === selectedCountry)?.[1]?.name || 'Global'
162+
153163
return (
154164
<div className="card space-y-6">
155165
<div className="flex flex-wrap items-center justify-between gap-3">
156166
<h3 className="font-semibold text-gray-200">Device fingerprint</h3>
157-
<div className="flex flex-wrap gap-2 items-center">
167+
<div className="flex flex-wrap gap-2 items-center relative">
158168
<div className="flex items-center gap-2">
159169
<label className="text-sm text-gray-400">Country:</label>
160-
<select
161-
value={selectedCountry}
162-
onChange={(e) => setSelectedCountry(e.target.value)}
163-
className="input btn-sm text-sm px-3 py-1.5 w-40"
164-
>
165-
<option value="">Global (random)</option>
166-
{countryOptions.map(([code, country]) => (
167-
<option key={code} value={code}>
168-
{country.name}
169-
</option>
170-
))}
171-
</select>
170+
<div className="relative w-48">
171+
<input
172+
type="text"
173+
placeholder="Search countries..."
174+
value={countrySearch || (selectedCountry ? selectedCountryName : '')}
175+
onChange={(e) => {
176+
setCountrySearch(e.target.value)
177+
setShowCountryDropdown(true)
178+
}}
179+
onFocus={() => setShowCountryDropdown(true)}
180+
className="input text-sm px-3 py-1.5 w-full"
181+
/>
182+
{showCountryDropdown && (
183+
<div className="absolute top-full left-0 right-0 mt-1 bg-gray-800 border border-gray-700 rounded shadow-lg z-10 max-h-56 overflow-y-auto">
184+
<div
185+
className="px-3 py-2 hover:bg-gray-700 cursor-pointer text-sm text-gray-300"
186+
onClick={() => {
187+
setSelectedCountry('')
188+
setCountrySearch('')
189+
setShowCountryDropdown(false)
190+
}}
191+
>
192+
🌍 Global (random)
193+
</div>
194+
{filteredCountries.map(([code, country]) => (
195+
<div
196+
key={code}
197+
className={clsx(
198+
'px-3 py-2 cursor-pointer text-sm',
199+
selectedCountry === code
200+
? 'bg-blue-900 text-blue-200'
201+
: 'text-gray-300 hover:bg-gray-700'
202+
)}
203+
onClick={() => {
204+
setSelectedCountry(code)
205+
setCountrySearch('')
206+
setShowCountryDropdown(false)
207+
}}
208+
>
209+
{country.name} ({code})
210+
</div>
211+
))}
212+
</div>
213+
)}
214+
</div>
172215
</div>
173216
<button
174217
type="button"

0 commit comments

Comments
 (0)