Files
clinicpro/docs/tasks/task-02-authentication/implementation_notes.md
T
hamed de1a78a235 feat: Implement SMS sending functionality with KavehNegar and Rangineh providers
- Add SendSmsMessage class for encapsulating SMS message data.
- Create KavehNegarProvider and RanginehProvider classes implementing SmsProviderInterface for sending SMS.
- Implement SmsLogRepository and SmsTemplateRepository for managing SMS logs and templates.
- Develop SendSmsHandler for handling SMS sending messages.
- Create SmsService to manage SMS dispatching and logging.
- Add UserProfileController for managing user profiles with CRUD operations.
- Implement UserProfile entity and repository for user profile data management.
- Update symfony.lock and bootstrap.php for project dependencies and environment setup.
2026-06-09 22:00:34 +03:30

4.8 KiB

نکات پیاده‌سازی — تسک ۰۲: ماژول احراز هویت

مهم‌ترین تفاوت با طراحی اولیه

جریان OTP با UUID (نه موبایل)

در Drupal، کد OTP با UUID ذخیره می‌شود، نه با شماره موبایل:

// ساختار ذخیره‌سازی در KeyValue/Redis:
key   = uuid (تولیدشده در send-code)
value = { "code": "12345", "mobile": "09120671713" }
TTL   = 1200 ثانیه

verify-code و oauth/token هر دو uuid می‌خواهند، نه موبایل.

// 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:

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:

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 (مقادیر واقعی)

// 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

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)

{
  "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 لاگین می‌کنند.