# نکات پیاده‌سازی — تسک ۰۲: ماژول احراز هویت ## مهم‌ترین تفاوت با طراحی اولیه ### جریان OTP با UUID (نه موبایل) در Drupal، کد OTP **با UUID** ذخیره می‌شود، نه با شماره موبایل: ```php // ساختار ذخیره‌سازی در KeyValue/Redis: key = uuid (تولیدشده در send-code) value = { "code": "12345", "mobile": "09120671713" } TTL = 1200 ثانیه ``` `verify-code` و `oauth/token` هر دو `uuid` می‌خواهند، نه موبایل. ```php // OtpService.php public function generate(string $mobile): array { $uuid = Uuid::uuid4()->toString(); $code = $this->isDev() ? '12345' : (string) random_int(10000, 99999); $this->redis->setex("otp:{$uuid}", 1200, json_encode([ 'code' => $code, 'mobile' => $mobile, ])); return ['uuid' => $uuid, 'code' => $code]; } public function verify(string $uuid, string $code): bool { $data = json_decode($this->redis->get("otp:{$uuid}"), true); if (!$data || $data['code'] !== $code) return false; $this->redis->del("otp:{$uuid}"); return true; } public function getMobileByUuid(string $uuid): ?string { $data = json_decode($this->redis->get("otp:{$uuid}"), true); return $data['mobile'] ?? null; } ``` ## MobileGrant در Symfony به جای OAuth2 کامل، یک custom JWT grant پیاده‌سازی کن: ``` POST /oauth/token grant_type=mobile uuid=... code=... client_id=clinic-pro client_secret=... → OtpService::verify(uuid, code) تأیید کند → getMobileByUuid(uuid) موبایل را بگیر → کاربر را پیدا یا بساز → JWT صادر کن ``` ## SMS Providers دو provider واقعی در Drupal: **KavehNegar:** ```php POST https://api.kavenegar.com/v1/{apiKey}/sms/send.json form: receptor={mobile}&message={code}&sender=10004346 ``` **Rangineh:** از پیاده‌سازی در `sms_provider/src/Plugin/SmsProvider/Rangineh.php` الگو بگیر. **Interface در Symfony:** ```php interface SmsProviderInterface { public function send(string $mobile, string $message): bool; } ``` پیکربندی در `.env`: ``` SMS_PROVIDER=kavenegar # kavenegar | rangineh | null (dev) KAVENEGAR_API_KEY=... KAVENEGAR_SENDER=10004346 ``` ## Rate Limiting (مقادیر واقعی) ```php // IP: 50 درخواست در ساعت // Mobile: 30 درخواست در ساعت // keys Redis: // rate_ip:{ip} TTL=3600 // rate_mobile:{mobile} TTL=3600 ``` ## TTL کد OTP: 1200 ثانیه (20 دقیقه) در طراحی اولیه اشتباهاً 120 ثانیه نوشته شده بود — مقدار واقعی از Drupal ۱۲۰۰ است. ## Flood Control (از Drupal) علاوه بر rate limiting، Drupal از flood control نیز استفاده می‌کند: - `oauth2_grant.mobile.failed_login_ip` — IP based - `oauth2_grant.mobile.failed_login_user` — User based در Symfony از Symfony's `RateLimiter` component جایگزین کن. ## سازگاری با کلاینت: GET /session/token ```php return new Response(bin2hex(random_bytes(22)), 200, ['Content-Type' => 'text/plain']); ``` ## مجوزها ``` DELETE /api/v1/user/{id} → ROLE_ADMIN PATCH /api/v1/user/{id} → owner یا ROLE_ADMIN ``` ## ساختار واقعی GET /oauth/userinfo (از سرور Drupal) ```json { "email": null, "email_verified": true, "username": "09120671710", "id": "22", "uuid": "d200f5c5-d717-4526-b263-d3bb7d0228d6", "created": "1762262151", "changed": "1762262267", "status": "1", "roles": { "0": "authenticated", "2": "doctor" }, "realName": "single doctor", "picture": [], "clinic_pro": { "base_role": "doctor", "db_uuid": "61be915b-595a-42e5-bca5-f80d22f4f14a", "db_key": "22bea8c1dc64d9b0c744810722519efe7290276ecd82d9bc650482aa4539bf0d", "my_doctors_uuid": { "uuid": "61be915b-595a-42e5-bca5-f80d22f4f14a", "id": "29", "name": "single doctor" } } } ``` ### نکات مهم userinfo: - `roles` یک **object** (نه array) با کلیدهای عددی است: `{"0": "authenticated", "2": "doctor"}` - `id` و `uuid` از users table - `realName` با R بزرگ (camelCase) - `clinic_pro.base_role` = نقش اصلی کاربر - `clinic_pro.db_uuid` = UUID موجودیت مرتبط (doctor/clinic/representation) - `clinic_pro.db_key` = token دسترسی برای عملیات داخلی - `clinic_pro.my_doctors_uuid` (فقط برای doctor) = مشخصات پروفایل دکتر ## نقش‌های مجاز برای login با password ``` فقط این نقش‌ها می‌توانند با POST /oauth/token (grant_type=password) لاگین کنند: - doctor - clinic - doctor_s_secretary ``` بقیه (patient, representation, admin) فقط از طریق OTP لاگین می‌کنند.