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

163 lines
4.8 KiB
Markdown

# نکات پیاده‌سازی — تسک ۰۲: ماژول احراز هویت
## مهم‌ترین تفاوت با طراحی اولیه
### جریان 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 لاگین می‌کنند.