- 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.
163 lines
4.8 KiB
Markdown
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 لاگین میکنند.
|