feat(doctor): claim captcha+mobile, owner profile delete, admin map zoom fix
- DoctorClaimController: ALTCHA CaptchaGuard on /claim (dev no-op via ALTCHA_ENABLED=false); optional `mobile` field must match the logged-in user's number (422 ERR_CONFLICT_001 on mismatch) - DoctorController::delete: now IS_AUTHENTICATED_FULLY — admin (any) or the owner of a claimed profile (IDOR-guarded); FK appointment guard kept - DoctorDetailPage address map: MapController calls map.invalidateSize() before flyTo (fixes needing to pick a city twice on a freshly-mounted map); geocode retries once (nominatim empty/429 on first hit) - tests: mobile mismatch, owner-delete allowed + others 403, unclaimed not deletable by random user - docs: doctor-claim.md (mobile+captcha), doctor.md (delete permission) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -449,15 +449,19 @@ function StarRating({ rate }: { rate: number }) {
|
||||
const IRAN_CENTER: [number, number] = [32.4279, 53.6880];
|
||||
|
||||
async function geocodeCityInIran(cityName: string): Promise<[number, number] | null> {
|
||||
try {
|
||||
const url = `https://nominatim.openstreetmap.org/search?q=${encodeURIComponent(cityName + ',ایران')}&format=json&countrycodes=ir&limit=1`;
|
||||
const res = await fetch(url, { headers: { 'Accept-Language': 'fa' } });
|
||||
const data = await res.json();
|
||||
if (data?.[0]) return [parseFloat(data[0].lat), parseFloat(data[0].lon)];
|
||||
return null;
|
||||
} catch {
|
||||
return null;
|
||||
const url = `https://nominatim.openstreetmap.org/search?q=${encodeURIComponent(cityName + ',ایران')}&format=json&countrycodes=ir&limit=1`;
|
||||
// nominatim گاهی روی اولین فراخوان خالی/۴۲۹ برمیگرداند؛ یک retry تا انتخاب اول هم کار کند.
|
||||
for (let attempt = 0; attempt < 2; attempt++) {
|
||||
try {
|
||||
const res = await fetch(url, { headers: { 'Accept-Language': 'fa' } });
|
||||
const data = await res.json();
|
||||
if (data?.[0]) return [parseFloat(data[0].lat), parseFloat(data[0].lon)];
|
||||
} catch {
|
||||
/* تلاش بعدی */
|
||||
}
|
||||
if (attempt === 0) await new Promise(r => setTimeout(r, 900));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function MapClickHandler({ onPick }: { onPick: (lat: number, lng: number) => void }) {
|
||||
@@ -467,8 +471,14 @@ function MapClickHandler({ onPick }: { onPick: (lat: number, lng: number) => voi
|
||||
|
||||
function MapController({ flyTarget }: { flyTarget: [number, number] | null }) {
|
||||
const map = useMap();
|
||||
// نقشهٔ تازهمانتشده ابعادش را نگرفته؛ flyTo بیاثر میماند تا invalidateSize صدا زده شود.
|
||||
useEffect(() => {
|
||||
if (flyTarget) map.flyTo(flyTarget, 12, { duration: 1.2 });
|
||||
map.invalidateSize();
|
||||
}, [map]);
|
||||
useEffect(() => {
|
||||
if (!flyTarget) return;
|
||||
map.invalidateSize();
|
||||
map.flyTo(flyTarget, 12, { duration: 1.2 });
|
||||
}, [flyTarget, map]);
|
||||
return null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user