feat(doctor): complete IRIMC import feature — claim flow, least-privilege importer, unique import key
- Extract import logic from AdminApiController into DoctorImportService (thin DoctorImportController keeps the same route/contract) - Surrogate users get marker role ROLE_UNCLAIMED_DOCTOR (+ backfill command app:doctors:backfill-surrogate-role) enabling safe deletion after claim - DB-level UNIQUE (source, medical_system_code) + concurrent-import retry - Doctor profile claim flow (climed.md): shahkar + PersonInfo identity checks via existing ApiIrService, Persian name normalization (PersianText), pessimistic-lock race protection, DoctorClaimRequest audit table (national code hashed, mobile masked), doctor_claim rate limiter, public claim-info endpoint, welcome SMS - Admin support tools: manual transfer endpoint + paginated doctor-claims audit list + owner_status filter/fields in admin doctors list - Least privilege: system owner now gets ROLE_IMPORTER (ROLE_ADMIN stripped), import endpoint accepts ADMIN|IMPORTER, isStaff includes IMPORTER - Headless crawler login: X-Service-Token header bypasses captcha only (rate limit + password checks intact; empty env = no bypass) - docs: doctor-claim.md (new), doctor-import.md, admin.md, doctor.md - tests: DoctorImportTest (6), DoctorClaimTest (11), PersianTextTest (5) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -30,8 +30,20 @@ class PasswordAuthenticator extends AbstractAuthenticator
|
||||
private readonly RateLimiterFactory $loginLimiter,
|
||||
private readonly CaptchaGuard $captcha,
|
||||
private readonly int $refreshTokenTtl = 2592000,
|
||||
private readonly ?string $crawlerServiceToken = null,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* لاگین سرویسی کرالر: هدر X-Service-Token با مقدار env CRAWLER_SERVICE_TOKEN
|
||||
* فقط کپچا را دور میزند — rate limit و اعتبارسنجی رمز دستنخورده میمانند.
|
||||
* env خالی = هیچ bypass (secure by default).
|
||||
*/
|
||||
private function isTrustedServiceLogin(Request $request): bool
|
||||
{
|
||||
return ($this->crawlerServiceToken ?? '') !== ''
|
||||
&& hash_equals($this->crawlerServiceToken, (string) $request->headers->get('X-Service-Token', ''));
|
||||
}
|
||||
|
||||
public function supports(Request $request): ?bool
|
||||
{
|
||||
return $request->getPathInfo() === '/api/v1/user/login'
|
||||
@@ -46,7 +58,9 @@ class PasswordAuthenticator extends AbstractAuthenticator
|
||||
}
|
||||
|
||||
// AppException را ExceptionSubscriber به پاسخ 422 با ERR_CAPTCHA_001 تبدیل میکند.
|
||||
$this->captcha->assertValid($request);
|
||||
if (!$this->isTrustedServiceLogin($request)) {
|
||||
$this->captcha->assertValid($request);
|
||||
}
|
||||
|
||||
$data = json_decode($request->getContent(), true) ?? [];
|
||||
$mobile = trim($data['mobile_number'] ?? '');
|
||||
|
||||
Reference in New Issue
Block a user