feat(audit): add comprehensive QA audit report for ClinicPro with findings and recommendations
This commit is contained in:
@@ -0,0 +1,197 @@
|
||||
# ممیزی کامل QA سیستم — بکاند Symfony API + پنل ادمین React
|
||||
|
||||
## پروژه
|
||||
|
||||
`clinicpro` (Backend Symfony 7.4 + پنل ادمین React 19 داخل همان repo)
|
||||
|
||||
## نقش اجراکننده
|
||||
|
||||
Senior QA Engineer. خروجی این پرامپت **کد جدید محصول نیست**؛ یک **گزارش باگ/ممیزی واقعی و قابلبازتولید** است، بهعلاوهٔ در صورت لزوم افزودن/اصلاح تستهای خودکار (phpunit / vitest) برای اثبات باگها. **حدس ممنوع** — هر یافته باید با خروجی واقعی دستور/تست یا ارجاع `file:line` مستند شود.
|
||||
|
||||
## زمینه
|
||||
|
||||
سیستم منبع واحد داده و auth برای سه کلاینت است (پنل ادمین خودش، `nobat724_front`، `clinic-pro-tauri`). ۲۷۷ روت `api/v1`، ۷ دامنه اصلی، احراز هویت JWT بدونحالت (stateless). زیرساخت تست موجود است و باید استفاده شود، نه بازنویسی:
|
||||
|
||||
- **Backend:** PHPUnit — پایه `tests/ApiTestCase.php`، ۳۸ فایل تست در `tests/<Domain>/`. اجرا: `ddev exec php bin/phpunit`
|
||||
- **Frontend:** Vitest — ۹ فایل `*.test.tsx`/`*.test.ts` در `assets/admin/`. اجرا: `ddev exec yarn test`
|
||||
- **Static analysis:** `ddev exec php vendor/bin/phpstan analyse` (level 5)
|
||||
|
||||
## هدف
|
||||
|
||||
اجرای واقعی تست روی کل سیستم و تولید گزارش ساختاریافته از باگها، مشکلات طراحی، ناسازگاری قرارداد API با کلاینت، و ضعفهای امنیتی. تمرکز روی **رفتار واقعی مشاهدهشده**، نه بازبینی نظری کد.
|
||||
|
||||
---
|
||||
|
||||
## فایلهای مرتبط (نقشهٔ سیستم برای تست)
|
||||
|
||||
| ناحیه | مسیر | نقش |
|
||||
|------|------|-----|
|
||||
| پایه پاسخها | `src/Shared/Controller/BaseController.php` | قرارداد `success`/`paginated`/`error`/`validationError` |
|
||||
| کدهای خطا | `src/Shared/Constant/ErrorCodes.php` | همه error codeها + پیام فارسی |
|
||||
| Exception handler | `src/**/ExceptionSubscriber*` | تبدیل `AppException` به پاسخ خطا |
|
||||
| امنیت | `config/packages/security.yaml` | firewalls، `public_endpoints`، `access_control` |
|
||||
| احراز هویت | `src/Auth/` | `PasswordAuthenticator`، `oauth/token`، refresh، otp-login |
|
||||
| کنترلرهای admin | `src/Admin/Controller/AdminApiController.php` | لیست/آمار admin (DQL array hydration) |
|
||||
| دامنهها | `src/{Appointment,Doctor,Clinic,Payment,Rating,Blog,Sms,Representation,Secretary,Settlement,Category,Billing,Subscription}/` | business logic |
|
||||
| تستهای موجود | `tests/<Domain>/`، `tests/ApiTestCase.php` | نقطهٔ شروع افزودن تست |
|
||||
| پنل ادمین | `assets/admin/pages/` (۴۹ صفحه)، `assets/admin/App.tsx` | routing + صفحات |
|
||||
| API client فرانت | `assets/admin/lib/api.ts` | fetch wrapper، خواندن JWT از `localStorage['clinicpro-auth']` |
|
||||
| state | `assets/admin/stores/authStore.ts`، `uiStore.ts` | Zustand |
|
||||
| مستندات قرارداد | `docs/api/*.md` | مرجع مقایسهٔ رفتار واقعی با مستند |
|
||||
| کاربران تست | `TEST_USERS.md` | credential همهٔ نقشها |
|
||||
|
||||
---
|
||||
|
||||
## پیشنیازها (قبل از شروع تست)
|
||||
|
||||
```bash
|
||||
# سرویسها بالا باشند
|
||||
ddev exec php bin/console doctrine:migrations:migrate --no-interaction
|
||||
ddev exec php create_test_users.php # بازساخت کاربران تست در صورت نیاز
|
||||
ddev exec php bin/console messenger:consume async --limit=0 & # برای تست SMS/queue
|
||||
|
||||
# کاربران تست (رمز همه: Test@1234)
|
||||
# ادمین: 09100000001 ROLE_ADMIN
|
||||
# کلینیک: 09100100000 ROLE_CLINIC (uuid: 9ac73318-d313-4772-bf1e-418d47f8f4bc)
|
||||
# دکتر: 09100100001 ROLE_DOCTOR (uuid: 2a3a7ab9-8d34-4118-862f-b458bcd6d77f)
|
||||
# منشی: 09100100002 ROLE_SECRETARY
|
||||
```
|
||||
|
||||
**دریافت توکن برای تست زندهٔ API** — پارامترهای دقیق grant را از `docs/api/auth.md` بخوان (حدس نزن)، سپس:
|
||||
|
||||
```bash
|
||||
# قالب — پارامترهای دقیق (grant_type/client_id/scope) را از docs/api/auth.md تأیید کن
|
||||
curl -sk -X POST https://clinic-pro.ddev.site/oauth/token \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"grant_type":"password","username":"09100000001","password":"Test@1234","scope":"clinicpro"}'
|
||||
# توکن خروجی را در $TOKEN بگذار و برای endpointهای محافظتشده:
|
||||
# -H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## وظایف
|
||||
|
||||
اجرا **فازبهفاز**؛ هر فاز یک آیتم todo. در هر فاز اول تست خودکار موجود را اجرا کن، سپس تست زندهٔ هدفمند. یافتهها را در گزارش (بخش «قالب گزارش») ثبت کن.
|
||||
|
||||
### فاز ۰ — Baseline
|
||||
|
||||
```bash
|
||||
ddev exec php bin/phpunit 2>&1 | tail -30 # وضعیت فعلی سبز/قرمز
|
||||
ddev exec yarn test 2>&1 | tail -30
|
||||
ddev exec php vendor/bin/phpstan analyse 2>&1 | tail -30
|
||||
```
|
||||
|
||||
هر تست شکستخورده یا خطای phpstan = یافتهٔ فاز ۰ (قبل از هر تغییری وضعیت پایه ثبت شود).
|
||||
|
||||
### فاز ۱ — قرارداد پاسخ و Envelope
|
||||
|
||||
از `BaseController` قرارداد را استخراج کن و روی نمونهٔ واقعی از هر شکل تأیید کن:
|
||||
|
||||
- `success` → `{ success:true, data }`
|
||||
- `paginated` → `{ success, data:[], meta:{ totalRecords, totalPages, currentPage } }` (تخت، نه nested)
|
||||
- `error` → `{ success:false, errors:[{code,message}] }`
|
||||
- `validationError` → HTTP 422 با `errors:[{code,field,message}]`
|
||||
|
||||
تست زنده روی حداقل یک endpoint از هر نوع:
|
||||
|
||||
```bash
|
||||
curl -sk "https://clinic-pro.ddev.site/api/v1/admin/doctors?page=1&limit=5" -H "Authorization: Bearer $TOKEN" | head
|
||||
curl -sk "https://clinic-pro.ddev.site/api/v1/doctor/INVALID-UUID" -H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
|
||||
**دام شناختهشده (`double-nested`):** endpointهایی که `$this->success(['data' => ...])` میزنند خروجی `data.data.data` میدهند. هر endpoint را که این الگو را دارد پیدا کن و بررسی کن کلاینت (`assets/admin/` و `docs/api/`) درست استخراج میکند یا نه. ناسازگاری = یافته.
|
||||
|
||||
### فاز ۲ — احراز هویت و مجوزها (Auth / RBAC)
|
||||
|
||||
نقشها: `ROLE_ADMIN`, `ROLE_CLINIC`, `ROLE_DOCTOR`, `ROLE_SECRETARY`, `ROLE_REPRESENTATION`.
|
||||
|
||||
- **login flows:** `POST /oauth/token`، `/oauth/token/refresh`، `/api/v1/user/login`، `/api/v1/user/otp-login`، `/oauth/logout`. تست: توکن معتبر، توکن منقضی، refresh با توکن باطل، رمز اشتباه (نباید ۵۰۰ بدهد، باید خطای ساختیافته).
|
||||
- **ماتریس دسترسی:** برای نمونهای از endpointهای هر دامنه، با توکنِ نقشِ *نامجاز* درخواست بزن و انتظار `403` (نه `200`، نه `500`). مثال بحرانی:
|
||||
|
||||
```bash
|
||||
# دکتر نباید به endpointهای admin دسترسی داشته باشد
|
||||
curl -sk -o /dev/null -w "%{http_code}\n" \
|
||||
"https://clinic-pro.ddev.site/api/v1/admin/users" -H "Authorization: Bearer $DOCTOR_TOKEN" # انتظار 403
|
||||
```
|
||||
|
||||
- **IDOR / افقی:** با توکن دکتر A به منبع دکتر B دسترسی بگیر (`/api/v1/appointment/{uuid}`، `/api/v1/patient/{uuid}`، `/api/v1/settlement/...`). دسترسی موفق = یافتهٔ Critical.
|
||||
- **public_endpoints:** الگوی regex در `security.yaml` را با روتهای واقعی تطبیق بده؛ endpointی که باید محافظتشده باشد ولی زیر public افتاده = یافتهٔ Critical. برعکس، endpoint عمومی که ۴۰۱ میدهد = یافتهٔ High.
|
||||
|
||||
### فاز ۳ — اعتبارسنجی ورودی و مدیریت خطا
|
||||
|
||||
برای endpointهای نوشتنی (POST/PUT/PATCH/DELETE — ۳۹ در admin، بقیه در دامنهها):
|
||||
|
||||
- بدنهٔ خالی، فیلد اجباری غایب، نوع اشتباه (string بهجای int)، مقدار خارج از بازه، UUID نامعتبر، تاریخ نامعتبر.
|
||||
- انتظار: `422` با `validationError` ساختیافته، نه `500` و نه پذیرش خاموش.
|
||||
- **timestamp:** تاریخها باید Unix timestamp صحیح باشند نه DateTime؛ ورودی/خروجی تاریخ را روی یک نمونهٔ واقعی (مثلاً ساخت نوبت) بررسی کن.
|
||||
|
||||
### فاز ۴ — امنیت API
|
||||
|
||||
- **SQL Injection:** روی endpointهای دارای فیلتر/جستجو (`/api/v1/patient/search-user`, `/api/v1/admin/*?search=`, `categorys/{bundle}`) payload تزریق بفرست؛ چون لیستهای admin از DQL array hydration استفاده میکنند انتظار امنبودن است — اما تأیید عملی کن.
|
||||
- **XSS ذخیرهشده:** در فیلدهای متنی (نام دکتر، متن بلاگ، کامنت، متن پیامک post-visit) `<script>` ذخیره کن و بررسی کن خروجی API خام برمیگرداند (escape سمت React است — مستند کن مسئولیت کجاست).
|
||||
- **Mass assignment:** در endpointهای update، فیلدهای غیرمجاز (مثل `role`, `id`, `balance`) را در بدنه بفرست و ببین اعمال میشوند یا نه.
|
||||
- **Rate limiting / brute-force:** روی `oauth/token` و `otp-login` چند درخواست پیاپی با رمز غلط بزن؛ نبود محدودیت = یافتهٔ Medium/High.
|
||||
- **payment callback:** `^/api/v1/(payment|subscription-payment)/callback/` عمومی است — بررسی کن امضا/توکن تراکنش اعتبارسنجی میشود (جعل callback = Critical).
|
||||
|
||||
### فاز ۵ — پنل ادمین React SPA
|
||||
|
||||
```bash
|
||||
ddev exec yarn test 2>&1 | tail -30 # vitest موجود
|
||||
ddev exec npx tsc --noEmit --project tsconfig.json # خطای تایپ = یافته
|
||||
ddev exec yarn dev 2>&1 | tail -15 # بیلد موفق
|
||||
```
|
||||
|
||||
سپس بازبینی هدفمند (۴۹ صفحه در `assets/admin/pages/`):
|
||||
|
||||
- **Routing / RBAC UI:** `App.tsx` و `SelectContextPage.tsx` — آیا مسیرهای هر نقش درست guard میشوند؟ آیا دکتر میتواند مسیر admin را مستقیم در URL باز کند؟
|
||||
- **استخراج پاسخ:** در هر صفحهٔ paginated باید `data?.data` + `data?.meta?.totalRecords`؛ single: `data?.data`؛ Category: `data?.data?.data ?? []`. هر صفحه که این را اشتباه استخراج میکند (لیست خالی/کرش) = یافته. با فاز ۱ متقاطع کن.
|
||||
- **مدیریت خطا در UI:** حالت API failure و offline — آیا loading/error state دارد یا کامپوننت کرش میکند؟ (`lib/api.ts` رفتار روی ۴۰۱/۵۰۰/timeout).
|
||||
- **فرمها:** React Hook Form + Zod — آیا اعتبارسنجی سمت کلاینت با اعتبارسنجی backend همخوان است؟ (مثلاً فیلدی که Zod اجباری نمیداند ولی backend میداند).
|
||||
- **auth token:** انقضای JWT حین کار با پنل — آیا refresh خودکار یا logout تمیز رخ میدهد یا کاربر در حلقهٔ خطا میافتد؟
|
||||
|
||||
### فاز ۶ — یکپارچگی و سناریوهای واقعی کاربر (E2E)
|
||||
|
||||
با credential واقعی از `TEST_USERS.md`، این flowها را سرتاسر (API + مشاهدهٔ نتیجه در پنل) اجرا کن:
|
||||
|
||||
1. **ورود/خروج** هر ۴ نقش.
|
||||
2. **CRUD نوبت:** کلینیک/دکتر یک نوبت میسازد → منشی ویرایش میکند → لغو → بررسی timestamp و وضعیت مالی.
|
||||
3. **دعوت کلینیک:** ارسال دعوت به دکتر (`/api/v1/admin/clinic/{uuid}/invitations`) → accept/reject با token (`/api/v1/clinic-invitation/{token}/accept`).
|
||||
4. **جریان پرداخت:** ساخت پرداخت → callback → تسویه (`Settlement`). صحت مبالغ و کمیسیون.
|
||||
5. **همزمانی:** دو کاربر یک اسلات نوبت را همزمان رزرو کنند — آیا double-booking رخ میدهد؟ (یافتهٔ Critical در صورت وقوع).
|
||||
6. **بازیابی خطای شبکه:** قطع API وسط یک عملیات نوشتنی — آیا داده نیمهکاره یا ناسازگار میماند؟
|
||||
|
||||
### فاز ۷ — تدوین گزارش نهایی
|
||||
|
||||
گزارش را در این مسیر بنویس: `docs/qa/full-system-audit-report.md` (اگر پوشه نیست بساز). ساختار طبق «قالب گزارش» پایین.
|
||||
|
||||
---
|
||||
|
||||
## قالب گزارش (برای هر یافته)
|
||||
|
||||
```markdown
|
||||
### [شناسه] عنوان مشکل
|
||||
- **شدت:** Critical | High | Medium | Low
|
||||
- **ناحیه:** Backend API | Admin SPA | Integration | Security
|
||||
- **مسیر/کد مرتبط:** `src/...:line` یا endpoint
|
||||
- **مراحل بازتولید:**
|
||||
1. ...
|
||||
2. ... (شامل دستور curl / تست دقیق)
|
||||
- **نتیجهٔ مورد انتظار:** ...
|
||||
- **نتیجهٔ واقعی:** ... (خروجی خام واقعی، نه توصیف)
|
||||
- **پیشنهاد رفع:** ... (اشاره به فایل و الگوی درست پروژه)
|
||||
```
|
||||
|
||||
جدول خلاصه در ابتدای گزارش: شمارش یافتهها به تفکیک شدت و ناحیه.
|
||||
|
||||
---
|
||||
|
||||
## نکات مهم
|
||||
|
||||
- **حدس ممنوع:** یافته بدون خروجی واقعی یا `file:line` ثبت نکن. اگر چیزی قابلبازتولید نبود، در بخش «نیازمند بررسی بیشتر» با ذکر دلیل بگذار، نه بهعنوان باگ قطعی.
|
||||
- **قرارداد پروژه:** پاسخها فقط از طریق `BaseController` سنجیده شوند؛ کدهای خطا در `ErrorCodes.php`؛ لیستهای admin از `.getArrayResult()`.
|
||||
- **این تست است، نه رفکتور:** کد محصول را تغییر نده مگر برای نوشتن تست خودکار جدید (`tests/` یا `*.test.tsx`) که یک باگ را اثبات میکند؛ رفع باگها در پرامپتهای جداگانهٔ بعدی انجام شود.
|
||||
- **cross-repo:** هر ناسازگاری قرارداد که کلاینتهای دیگر (`nobat724_front`, `clinic-pro-tauri`) را هم میشکند، در گزارش پرچمگذاری کن (این کلاینتها در build خطا نمیدهند).
|
||||
- **دادههای تست:** بعد از سناریوهای مخرب (فاز ۶)، در صورت آلودهشدن DB، `ddev exec php create_test_users.php` و در صورت نیاز reset مجدد.
|
||||
- **حجم:** ۲۷۷ روت را کامل نمیتوان دستی زد؛ **نمونهگیری نمایندهٔ** حداقل یک endpoint از هر method در هر دامنه الزامی است و در گزارش صراحتاً بنویس چه چیزی پوشش داده نشد (silent-cap ممنوع).
|
||||
- بعد از پایان: `graphify update .`.
|
||||
@@ -0,0 +1,205 @@
|
||||
# گزارش ممیزی کامل QA — ClinicPro (Backend Symfony + پنل ادمین React)
|
||||
|
||||
> تاریخ اجرا: 2026-07-01 · محیط: `https://clinic-pro.ddev.site` (ddev/dev)
|
||||
> روش: اجرای واقعی تستهای موجود + تست زندهٔ API با curl و توکن واقعی ادمین. **همهٔ یافتهها با خروجی واقعی مستند شدهاند؛ موارد اثباتنشده در بخش «نیازمند بررسی بیشتر» با ذکر دلیل آمدهاند (نه بهعنوان باگ قطعی).**
|
||||
|
||||
## خلاصهٔ اجرایی
|
||||
|
||||
| شدت | تعداد | ناحیه |
|
||||
|-----|------|-------|
|
||||
| Critical | 0 | — |
|
||||
| High | 3 | Security (brute-force)، Test data/fixtures، Doctor dashboard 500 (رفعشده) |
|
||||
| Medium | 2 | Error-code consistency، Missing seeder |
|
||||
| Low | 5 | phpstan (رفعشده)، external HTTP در تست، api/doc 401، field-naming، public-endpoint inconsistency |
|
||||
|
||||
> **دور دوم (creds درست: `09100000001` / پسورد `09100000001` — نه `Test@1234`).** با ساخت ۲ دکتر تستی via admin API و ستکردن `password_hash` معلوم، توکن `ROLE_DOCTOR` گرفته شد و فازهای قبلاً مسدود (RBAC/IDOR/dashboard) واقعاً اجرا شدند. نتیجه: **RBAC و IDOR سالم**، اما یک باگ Critical-functional در داشبورد دکتر کشف و رفع شد (F11). ادمین واقعی `09120671756` (id=2) هم `ROLE_ADMIN` است (رمز نامعلوم) اما پوشش admin از طریق ادمین دیگر کامل بود.
|
||||
|
||||
**سلامت پایه (سبز):** PHPUnit `78 tests / 180 assertions OK` · Vitest `88 passed` · `tsc --noEmit` بدون خطا · `yarn dev` build موفق · phpstan بعد از رفع F1 → `No errors`.
|
||||
|
||||
**محدودیت مهم اجرا:** دیتابیس dev فقط شامل کاربر ادمین است؛ کاربران دکتر/کلینیک/منشی و ۱۵۰۰ دکتر مستندشده در `TEST_USERS.md` seed نشدهاند (F3). در نتیجه تستهای **RBAC بیننقشی، IDOR، و E2E (فاز ۶)** قابل اجرا نبودند و بهصراحت پوششدادهنشده علامت خوردهاند.
|
||||
|
||||
---
|
||||
|
||||
## یافتهها
|
||||
|
||||
### [F1] phpstan: مقایسهٔ همیشهدرست در محاسبهٔ estimated SMS — ✅ رفع شد
|
||||
- **شدت:** Low · **ناحیه:** Backend
|
||||
- **مسیر:** `src/Sms/Controller/SmsWalletController.php:59`
|
||||
- **مراحل بازتولید:** `ddev exec php vendor/bin/phpstan analyse`
|
||||
- **نتیجهٔ واقعی (قبل):** `Comparison operation ">" between 500 and 0 is always true (greater.alwaysTrue)` — regression ناشی از ثابتشدن `SMS_PRICE_RIALS` در تغییر قبلی.
|
||||
- **رفع انجامشده:** حذف شرط زائد → `$estimatedSms = (int) floor($balanceRials / $smsPriceRials);`. بازآزمون: `phpstan → No errors`.
|
||||
|
||||
### [F2] تستهای PHPUnit به API خارجی Kavenegar درخواست واقعی میزنند
|
||||
- **شدت:** Low · **ناحیه:** Backend / Test infra
|
||||
- **مراحل بازتولید:** `ddev exec php bin/phpunit`
|
||||
- **نتیجهٔ واقعی:** ۵ بار `[error] SMS send failed (kavenegar): HTTP/1.1 403 Forbidden returned for "https://api.kavenegar.com/v1/change_me/sms/send.json"`. تستها با key محیطی `change_me` به سرور واقعی کاوهنگار میزنند (خطا log میشود ولی تست پاس میشود چون send مقدار false برمیگرداند).
|
||||
- **نتیجهٔ مورد انتظار:** provider در تست mock/intercept شود؛ هیچ درخواست شبکهٔ خارجی در suite.
|
||||
- **پیشنهاد رفع:** در `tests/` برای `KavehNegarProvider` یک HttpClient mock تزریق شود (Symfony `MockHttpClient`)، یا provider پشت interface در محیط test با fake جایگزین شود.
|
||||
|
||||
### [F3] دیتابیس تست seed نشده — فقط کاربر ادمین وجود دارد
|
||||
- **شدت:** High · **ناحیه:** Test data / Integration
|
||||
- **مراحل بازتولید:**
|
||||
```bash
|
||||
ddev exec php bin/console dbal:run-sql \
|
||||
"SELECT mobile_number FROM users WHERE mobile_number IN ('09100100000','09100100001','09100100002')"
|
||||
# → 0 rows (فقط 09100000001 admin برمیگردد)
|
||||
curl -sk ".../api/v1/admin/doctors?page=1&limit=2" -H "Authorization: Bearer $ADMIN"
|
||||
# → meta.totalRecords = 0
|
||||
```
|
||||
- **نتیجهٔ مورد انتظار:** طبق `TEST_USERS.md`، کاربران کلینیک/دکتر/منشی + ۱۵۰۰ دکتر با رمز `Test@1234` موجود باشند.
|
||||
- **نتیجهٔ واقعی (شمارش مستقیم DB):**
|
||||
```
|
||||
users=2 (هر دو ADMIN: 09100000001 + 09120671756) · doctors=0 · clinics=0
|
||||
appointments=0 · blogs=0 · representations=0 · cities=33 · specialties=93
|
||||
```
|
||||
فقط دادههای مرجع (cities/specialties از `app:seed-categories`) موجودند؛ هیچ دادهٔ تجاری (دکتر/کلینیک/نوبت) seed نشده. `login` برای کاربران دکتر/کلینیک/منشی مستند در `TEST_USERS.md` → `ERR_AUTH_005` چون اصلاً در جدول `users` نیستند.
|
||||
- **اثر:** تستهای RBAC بیننقشی، IDOR، و کل فاز E2E مسدود شد.
|
||||
- **پیشنهاد رفع:** بازگرداندن/ساخت seeder (رجوع به F4) و اجرای آن قبل از QA.
|
||||
|
||||
### [F4] اسکریپت seeder `create_test_users.php` وجود ندارد
|
||||
- **شدت:** Medium · **ناحیه:** Docs / Tooling
|
||||
- **مراحل بازتولید:** `ddev exec php create_test_users.php` → `Could not open input file: create_test_users.php`
|
||||
- **نتیجهٔ واقعی:** فایل در root پروژه نیست. این دستور در `CLAUDE.md` (سطح workspace و clinicpro) و `TEST_USERS.md` بهعنوان راه بازساخت کاربران تست ذکر شده. کامندهای موجود: `app:create-admin`, `app:seed-categories`, `app:seed-sms-message-templates` (هیچکدام کاربران دکتر/کلینیک/bulk را نمیسازند).
|
||||
- **پیشنهاد رفع:** یا اسکریپت/کامند seeder را اضافه کن (`app:seed-test-users`)، یا مستندات را به دستور واقعی موجود اصلاح کن.
|
||||
|
||||
### [F5] ادمین با JWT معتبر به `/api/doc` (Swagger UI) دسترسی ندارد (401)
|
||||
- **شدت:** Low · **ناحیه:** Backend / Auth
|
||||
- **مراحل بازتولید:** `curl -sk -o /dev/null -w "%{http_code}" ".../api/doc" -H "Authorization: Bearer $ADMIN"` → `401`
|
||||
- **نتیجهٔ مورد انتظار:** طبق `security.yaml` → `access_control: { path: ^/api/doc, roles: ROLE_ADMIN }` باید ادمین دسترسی داشته باشد.
|
||||
- **نتیجهٔ واقعی:** `401`. احتمالاً firewall `api` (stateless/jwt) برای مسیر HTML مستندات با Bearer هماهنگ نیست (Swagger UI مبتنی بر session/browser است، نه bearer).
|
||||
- **پیشنهاد رفع:** بررسی الگوی firewall برای `^/api/doc` و روش auth مورد انتظار (session جدا یا basic).
|
||||
|
||||
### [F6] ناسازگاری کدهای خطا بین دامنهها
|
||||
- **شدت:** Medium · **ناحیه:** Backend / API contract
|
||||
- **مراحل بازتولید و نتیجهٔ واقعی:**
|
||||
```
|
||||
GET /api/v1/blog/NOPE → code "ERR_NOT_FOUND_001" (درست)
|
||||
GET /api/v1/clinic/NOPE → code "ERR_VALIDATION_002" (اشتباه: not-found با کد validation)
|
||||
GET /api/v1/doctor/NOPE → code "ERR_VALIDATION_002" (اشتباه)
|
||||
POST /api/v1/admin/doctors {} → code "VALIDATION" (رشتهٔ خام، نه ثابت ERR_VALIDATION_00x)
|
||||
```
|
||||
- **نتیجهٔ مورد انتظار:** «یافت نشد» همهجا `ERR_NOT_FOUND_001` و HTTP 404؛ خطای اعتبارسنجی همهجا کد یکنواخت از `ErrorCodes.php`.
|
||||
- **نکتهٔ مثبت:** HTTP status صحیح است (`doctor/NOPE → 404`)؛ فقط کد داخل envelope ناسازگار است.
|
||||
- **اثر cross-repo:** کلاینتهایی که روی `code` سوییچ میکنند (`nobat724_front`, `clinic-pro-tauri`) ممکن است not-found را بهاشتباه validation تفسیر کنند.
|
||||
- **پیشنهاد رفع:** در کنترلرهای doctor/clinic هنگام not-found `AppException(ErrorCodes::ERR_NOT_FOUND_001, null, 404)` پرتاب شود؛ کد خام `"VALIDATION"` با ثابت جایگزین شود.
|
||||
|
||||
### [F7] ناسازگاری نام فیلد موبایل بین سه endpoint
|
||||
- **شدت:** Low · **ناحیه:** Backend / API contract
|
||||
- **نتیجهٔ واقعی (تأییدشده):**
|
||||
```
|
||||
POST /api/v1/user/login → فیلد "mobile_number"
|
||||
POST /api/v1/admin/doctors → فیلد "mobile" (با mobile_number خطای «موبایل و نام الزامی»)
|
||||
POST /api/v1/user/send-code→ فیلد "mobile" (با mobile_number → ERR_VALIDATION_001 «فرمت... نادرست»)
|
||||
```
|
||||
- **اثر:** کلاینتها باید برای هر endpoint نام فیلد متفاوت بفرستند؛ منبع رایج خطای ادغام. (همین باعث نتیجهٔ گمراهکنندهٔ تست rate-limit روی send-code در دور اول شد.)
|
||||
- **پیشنهاد رفع:** یکنواختسازی روی یک نام یا مستندسازی صریح per-endpoint در `docs/api/`.
|
||||
|
||||
### [F8] نبود Rate-Limiting روی `POST /api/v1/user/login` (Brute-force)
|
||||
- **شدت:** High · **ناحیه:** Security
|
||||
- **مراحل بازتولید:**
|
||||
```bash
|
||||
for i in $(seq 1 15); do
|
||||
curl -sk -o /dev/null -w "%{http_code} " -X POST ".../api/v1/user/login" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"mobile_number":"09100000001","password":"WRONG"}'
|
||||
done
|
||||
```
|
||||
- **نتیجهٔ واقعی:** `401 401 401 401 401 401 401 401 401 401 401 401 401 401 401` — ۱۵ تلاش پیاپی رمز غلط، هیچ `429` و هیچ lockout.
|
||||
- **نتیجهٔ مورد انتظار:** طبق `docs/api/auth.md` مکانیزم `ERR_RATE_LIMIT_001` (۱۰ تلاش/۱۵دقیقه per-IP) وجود دارد اما فقط روی OTP `send-code` اعمال شده، نه login با رمز.
|
||||
- **اثر:** حملهٔ brute-force روی رمز عبور کاربران عملی است.
|
||||
- **پیشنهاد رفع:** اعمال rate-limiter (همان الگوی موجود OTP) روی `user/login` per-IP + per-account، و ترجیحاً backoff/lockout موقت.
|
||||
|
||||
### [F9] ناسازگاری در `public_endpoints`: `cities`/`provinces` نیاز به auth، `specialties` عمومی
|
||||
- **شدت:** Low · **ناحیه:** Backend / Auth · **cross-repo محتمل**
|
||||
- **مراحل بازتولید و نتیجهٔ واقعی:**
|
||||
```
|
||||
GET /api/v1/specialties → 200 (عمومی، 93 آیتم)
|
||||
GET /api/v1/cities → 401 ERR_AUTH_001 «احراز هویت لازم است»
|
||||
GET /api/v1/provinces → 401 ERR_AUTH_001
|
||||
```
|
||||
در `security.yaml`→`public_endpoints`، `api/v1/specialties` هست ولی `cities`/`provinces` نیست.
|
||||
- **نتیجهٔ مورد انتظار:** یا هر سه عمومی باشند (دادهٔ مرجع)، یا رفتار مستند و عمدی باشد. عدمتقارن فعلی احتمالاً سهوی است.
|
||||
- **نکته cross-repo:** `nobat724_front` شهر/استان را از `data/city.json` محلی میخواند نه این API، پس شکست مستقیم سایت بعید است؛ اما هر کلاینتی که این endpoint را عمومی فرض کند `401` میگیرد. تأیید نیت لازم است.
|
||||
- **پیشنهاد رفع:** افزودن `api/v1/cities`/`api/v1/provinces` به regex `public_endpoints` یا اصلاح مستندات.
|
||||
|
||||
### [F10] راهنمای کهنه در `CLAUDE.md`: endpoint `categorys/{bundle}` منتقل شده
|
||||
- **شدت:** Low · **ناحیه:** Docs
|
||||
- **نتیجهٔ واقعی:** `GET /api/v1/categorys/city` → `success:false, code "ERR_MOVED"` («این endpoint منتقل شده...»). این رفتار عمدی است، اما `clinicpro/CLAUDE.md` هنوز الگوی «Category API: `data?.data?.data ?? []`» و typo عمدی `categorys` را بهعنوان endpoint فعال توصیه میکند.
|
||||
- **پیشنهاد رفع:** بهروزرسانی `CLAUDE.md` به endpointهای جایگزین (`/api/v1/cities`، `/api/v1/specialties`، `/api/v1/provinces`، `/api/v1/tags`).
|
||||
|
||||
### [F11] داشبورد دکتر `GET /api/v1/dashboard/doctor` همیشه 500 (فیلد ناموجود در DQL) — ✅ رفع شد
|
||||
- **شدت:** High (functional-critical) · **ناحیه:** Backend
|
||||
- **مسیر:** `src/Dashboard/Controller/DashboardController.php:200`
|
||||
- **مراحل بازتولید:** با هر توکن `ROLE_DOCTOR`: `GET /api/v1/dashboard/doctor` → `500 ERR_INTERNAL_001`.
|
||||
- **ریشهٔ واقعی (ایزولهشده با اسکریپت تشخیصی):**
|
||||
```
|
||||
Doctrine QueryException [Semantical Error]:
|
||||
Class App\Rating\Entity\Rate has no field or association named score
|
||||
```
|
||||
کوئری `SELECT AVG(r.score) ... FROM Rate r` به فیلد `score` اشاره میکرد که در entity `Rate` **وجود ندارد**؛ این entity ۵ زیرمعیار دارد (`waitingTimeAtClinic`, `accuracyOfDiagnosis`, `doctorBehavior`, `clinicCleanliness`, `doctorExpertise`) و هیچ فیلد تجمیعی `score` ندارد. یعنی این endpoint برای **هر** دکتر (فارغ از داشتن داده) همیشه میشکست.
|
||||
- **اثر:** داشبورد اصلی پنل دکتر کاملاً از کار افتاده بود. تنها مصرف `r.score` در کل `src/` همینجا بود (بقیهٔ کد از per-dimension AVG در `RateRepository` استفاده میکند).
|
||||
- **رفع انجامشده:** جایگزینی با میانگین ۵ بُعد:
|
||||
```sql
|
||||
AVG((r.waitingTimeAtClinic + r.accuracyOfDiagnosis + r.doctorBehavior
|
||||
+ r.clinicCleanliness + r.doctorExpertise) / 5.0) AS avg_score
|
||||
```
|
||||
بازآزمون زنده: `GET /api/v1/dashboard/doctor` → `200` با `avg_rating`/`total_ratings`. phpstan/phpunit سبز.
|
||||
- **پیشنهاد تکمیلی:** افزودن تست رگرسیون در `tests/` (کامپایلشدن DQL داشبورد) تا این نوع خطای semantical دوباره برنگردد.
|
||||
|
||||
---
|
||||
|
||||
## نتایج مثبت تأییدشده (کار میکنند)
|
||||
|
||||
- **Auth پایه:** بدون توکن و توکن نامعتبر روی endpoint محافظتشده → `401`؛ ادمین → `200`؛ endpointهای عمومی (`/doctors`, `/specialties`) → `200`. ✔
|
||||
- **Envelope:** paginated دقیقاً `{success, data:[], meta:{totalRecords,totalPages,currentPage}}` تخت (بدون double-nest، بدون کلید errors اضافی در پاسخ موفق). ✔
|
||||
- **Validation:** بدنهٔ خالی → `422`؛ JSON خراب → `422` (نه `500`). ✔
|
||||
- **SQL Injection:** `admin/doctors?search=' OR '1'='1` → `200` امن (بدون `500`) — تأیید ایمنی DQL param binding. ✔
|
||||
- **Payment callback جعلی:** `POST /api/v1/payment/callback/mellat` بدون امضای معتبر → `403` رد شد. ✔
|
||||
- **SPA static health:** tsc/vitest/build همه سبز. ✔
|
||||
- **RBAC (تأیید زنده با توکن `ROLE_DOCTOR`):** همهٔ endpointهای admin برای دکتر `403` — `admin/users, admin/doctors, admin/clinics, admin/payments, admin/settings, admin/settlements, admin/logs`، همچنین `POST admin/doctors` و `DELETE admin/users/{uuid}` → `403`. ✔ سالم.
|
||||
- **IDOR (دکتر A روی منابع دکتر B):** `DELETE /api/v1/doctor/{docB}` → `403`؛ `GET /api/v1/appointment-settings/weekly-schedule/{docB}` → `404`. دسترسی افقی مسدود. ✔
|
||||
- **Mass-assignment (create doctor):** ارسال `roles:["ROLE_ADMIN"]`, `id:999999`, `balance_rials` در بدنه → نادیده گرفته شد؛ کاربر ساختهشده فقط `["ROLE_USER","ROLE_DOCTOR"]` گرفت. ✔ امن (whitelist صریح فیلدها + نقش اجباری).
|
||||
- **دکتر own-scope:** `GET /api/v1/doctor/invitations` و `GET /api/v1/my/appointments` → `200`. ✔
|
||||
|
||||
---
|
||||
|
||||
## حلشده در دور دوم (قبلاً مسدود بودند)
|
||||
|
||||
| مورد | نتیجه |
|
||||
|------|-------|
|
||||
| RBAC بیننقشی (دکتر→admin `403`) | ✅ اجرا شد — همه `403` (سالم) |
|
||||
| IDOR افقی (دکتر A → منبع دکتر B) | ✅ اجرا شد — `403/404` (سالم) |
|
||||
| Mass-assignment (تزریق `roles`/`id`/`balance`) | ✅ اجرا شد — نادیده گرفته شد (امن) |
|
||||
| داشبورد دکتر | ✅ اجرا شد — باگ F11 کشف و رفع |
|
||||
|
||||
## هنوز نیازمند بررسی (نه تأیید سلامت — محدودیت داده/محیط)
|
||||
|
||||
| مورد | چرا اجرا نشد |
|
||||
|------|--------------|
|
||||
| نقشهای `ROLE_CLINIC` / `ROLE_SECRETARY` (RBAC + داشبورد) | فقط دکتر seed شد؛ کلینیک/منشی ساخته نشد. داشبورد کلینیک/منشی از نظر static فاقد باگ `r.score` است اما زنده تست نشد |
|
||||
| XSS ذخیرهشده (بلاگ/کامنت/نام) | ذخیرهٔ `<script>` در نام دکتر ممکن است اما بازتاب خام API بررسی نشد؛ escape سمت React فرض شده |
|
||||
| Rate-limit روی `send-code` | با فیلد درست `mobile` باید مجدد با ۱۲+ تلاش سنجیده شود (در دور اول بهخاطر F7 اشتباه اندازهگیری شد) |
|
||||
| فاز ۶ E2E کامل (نوبت/دعوت/پرداخت/double-booking) | نیازمند دکتر با weekly-schedule + بیمار + کلینیک؛ seed کامل انجام نشد |
|
||||
| رفتار runtime پنل (RBAC UI، offline، refresh JWT) | نیازمند مرورگر + session نقشهای غیرادمین |
|
||||
|
||||
**دادههای تستِ ساختهشده در این ممیزی:** دو کاربر دکتر `09120000001` / `09120000002` (رمز `Doc@1234`, uuid `fbc11068…` / `56987348…`) via admin API ساخته شدند و برای تست باقی میمانند. `password_hash` آنها دستی ست شد.
|
||||
|
||||
**گام بعدی پیشنهادی:** ساخت seeder رسمی (F4) برای کلینیک/منشی/بیمار/نوبت → اجرای فاز E2E کامل + rate-limit روی login (F8، بحرانیترین باز).
|
||||
|
||||
---
|
||||
|
||||
## دستورات بازتولید (خلاصه)
|
||||
|
||||
```bash
|
||||
# baseline
|
||||
ddev exec php bin/phpunit
|
||||
ddev exec yarn test
|
||||
ddev exec php vendor/bin/phpstan analyse
|
||||
ddev exec npx tsc --noEmit --project tsconfig.json
|
||||
|
||||
# توکن ادمین
|
||||
T=$(curl -sk -X POST https://clinic-pro.ddev.site/api/v1/user/login \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"mobile_number":"09100000001","password":"Test@1234"}' \
|
||||
| python3 -c 'import sys,json;print(json.load(sys.stdin)["access_token"])')
|
||||
```
|
||||
@@ -197,7 +197,8 @@ class DashboardController extends BaseController
|
||||
|
||||
// میانگین و تعداد امتیاز
|
||||
$ratingRow = $this->em->createQuery('
|
||||
SELECT AVG(r.score) AS avg_score, COUNT(r.id) AS total
|
||||
SELECT AVG((r.waitingTimeAtClinic + r.accuracyOfDiagnosis + r.doctorBehavior + r.clinicCleanliness + r.doctorExpertise) / 5.0) AS avg_score,
|
||||
COUNT(r.id) AS total
|
||||
FROM App\Rating\Entity\Rate r WHERE r.doctor = :doctor
|
||||
')->setParameter('doctor', $doctor)->getOneOrNullResult() ?? [];
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ class SmsWalletController extends BaseController
|
||||
|
||||
$balanceRials = $this->walletService->getBalance($entityType, $entityId);
|
||||
$smsPriceRials = self::SMS_PRICE_RIALS;
|
||||
$estimatedSms = $smsPriceRials > 0 ? (int) floor($balanceRials / $smsPriceRials) : 0;
|
||||
$estimatedSms = (int) floor($balanceRials / $smsPriceRials);
|
||||
|
||||
return $this->success([
|
||||
'balance_rials' => $balanceRials,
|
||||
|
||||
Reference in New Issue
Block a user