feat: add BlogBodySanitizer for HTML sanitization on article save
- Implemented BlogBodySanitizer to clean HTML content before saving articles, ensuring security against XSS attacks. - Added tests for BlogBodySanitizer to verify that unsafe tags and attributes are stripped from the content. - Introduced ApiLeastPrivilegeTest to ensure that unauthorized users cannot access sensitive API routes, maintaining strict access control.
This commit is contained in:
@@ -0,0 +1,520 @@
|
|||||||
|
# آدیت امنیتی دلتایی — سطح حملهٔ ساختهشده بعد از ۲۰۲۶-۰۷-۱۹
|
||||||
|
|
||||||
|
## زمینه
|
||||||
|
|
||||||
|
آخرین آدیت امنیتی `clinicpro` در `docs/security/AUDIT-2026-07-19.md` ثبت شده. از آن تاریخ تا
|
||||||
|
امروز (۲۰۲۶-۰۸-۰۷) روی این repo **۳۵۴ کامیت** زده شده و تمرکز غالب آنها دقیقاً روی سطحی است
|
||||||
|
که آدیت قبلی ندیده بود:
|
||||||
|
|
||||||
|
- رجیستری واحد مجوزها (`PermissionCatalog`) و اندپوینت `GET /api/v1/permission-catalog`
|
||||||
|
- بازنویسی گیتهای منشی و پزشکِ مهمانِ کلینیک
|
||||||
|
- دامنهٔ کاملاً جدید `Treatment` — پروندهٔ درمان، پروتکل، اجرای جلسه توسط پرسنل
|
||||||
|
- نقش/پنل `staff` با گاردِ متمرکز `StaffRouteGuardSubscriber`
|
||||||
|
- `PatientRecordScopeResolver` برای محدودکردن دید پروندهها
|
||||||
|
|
||||||
|
آدیت قبلی خودش در بخش «محدودیت پوشش» نوشته بود که ماتریس authz ناقص مانده، چون فقط کاربر
|
||||||
|
`admin` و `doctor` در DB بود. آن محدودیت حالا برطرفشدنی است.
|
||||||
|
|
||||||
|
این پرامپت **آدیت کامل از صفر نیست**. عمداً دلتایی است: یافتههای قبلی فقط regression میشوند،
|
||||||
|
و بودجهٔ اصلی صرف کدی میشود که هرگز آدیت نشده.
|
||||||
|
|
||||||
|
## مشکل / هدف
|
||||||
|
|
||||||
|
**مشکل:** بزرگترین سطح حملهٔ فعلی پروژه — authorization چندنقشی و دامنهٔ `Treatment` — هیچوقت
|
||||||
|
تست امنیتی نشده. یک رجیستری مجوز که در UI رندر میشود ولی در backend enforce نشود، یعنی هر
|
||||||
|
منشی/پزشکِ مهمان میتواند با یک درخواست مستقیم به API از مجوزش فرار کند.
|
||||||
|
|
||||||
|
**هدف:** پیدا کردن و رفع آسیبپذیریهای این سطح، بهعلاوهٔ تأیید اینکه یافتههای آدیت قبلی
|
||||||
|
برنگشتهاند.
|
||||||
|
|
||||||
|
**سیاست رفع (تصمیم کاربر):**
|
||||||
|
|
||||||
|
- 🟥 Critical و 🟧 High: **همان جلسه خودکار رفع شود** + تست رگرسیون نوشته شود.
|
||||||
|
- 🟨 Medium و پایینتر: **اول گزارش، بعد تأیید کاربر، بعد رفع.** بدون تأیید دست نزن.
|
||||||
|
|
||||||
|
**خارج از محدوده (تصمیم کاربر):** مهاجرت CKEditor از `@ckeditor/ckeditor5-build-classic` به
|
||||||
|
پکیج umbrella `ckeditor5` v45+. فقط بهعنوان «risk پذیرفتهشده» در گزارش ثبت شود، پیاده نشود.
|
||||||
|
|
||||||
|
## معیار پذیرش
|
||||||
|
|
||||||
|
- ✅ **موفق:** گزارش `docs/security/AUDIT-2026-08-07.md` تولید شده و برای **هر** یافته یک بازتولید
|
||||||
|
اجراشده دارد (دستور + خروجی واقعی). هر ردیف `PermissionCatalog::RESOURCES` یک تست دارد که
|
||||||
|
ثابت میکند خاموشبودن آن مجوز، درخواستِ متناظر API را با **403** رد میکند — نه اینکه فقط
|
||||||
|
دکمه را در UI پنهان کند.
|
||||||
|
- ❌ **خطا:** درخواست به هر اندپوینت `/api/v1/treatment-*` با توکن کاربری از tenant دیگر →
|
||||||
|
**403 یا 404** با envelope خطای `BaseController` و کد از `ErrorCodes`؛ هرگز 200 با دادهٔ
|
||||||
|
tenant دیگر و هرگز 500 با stack trace.
|
||||||
|
- ⚠️ **مرزی:** کاربر چندنقشی (مثلاً هم `ROLE_STAFF` هم `ROLE_SECRETARY`) پس از
|
||||||
|
`POST /api/v1/auth/switch-context` دقیقاً دسترسی همان context فعال را دارد، نه اجتماع دو
|
||||||
|
نقش. همچنین کاربری که **فقط** `ROLE_STAFF` است، روی هر مسیر خارج از allowlist ــ از جمله
|
||||||
|
مسیرهایی که بعد از نوشتن گارد اضافه شدهاند ــ 403 میگیرد.
|
||||||
|
- ✅ تستها سبزند: `ddev exec php bin/phpunit` و `ddev exec php vendor/bin/phpstan analyse`.
|
||||||
|
- ✅ اگر رفتار یا قرارداد هر endpoint عوض شد، فایل متناظر در `docs/api/` همان جلسه بهروز شد.
|
||||||
|
|
||||||
|
## فایلهای مرتبط
|
||||||
|
|
||||||
|
| فایل | نقش |
|
||||||
|
|------|-----|
|
||||||
|
| `../.claude/skills/symfony-security-audit/driver.mjs` | درایور آدیت؛ **در روت workspace است، نه در `clinicpro/`** |
|
||||||
|
| `.claude/skills/qa-clinicpro/driver.mjs` | ساخت/بررسی اکانت نقشها (`roles`) |
|
||||||
|
| `docs/security/AUDIT-2026-07-19.md` | یافتههای قبلی برای regression |
|
||||||
|
| `docs/security-audit.md` | آدیت ۲۰۲۶-۰۶-۰۹ (قدیمیتر) |
|
||||||
|
| `src/Shared/Security/PermissionCatalog.php` | منبع واحد منابع/اکشنهای مجوزدهی |
|
||||||
|
| `src/Shared/Controller/PermissionCatalogController.php` | `GET /api/v1/permission-catalog` |
|
||||||
|
| `src/Secretary/Security/SecretaryPermissionChecker.php` | enforce مجوز منشی |
|
||||||
|
| `src/Secretary/Security/SecretaryAccessChecker.php` | حل tenant منشی |
|
||||||
|
| `src/Clinic/Security/ClinicDoctorPermissionChecker.php` | enforce مجوز پزشکِ مهمان |
|
||||||
|
| `src/Clinic/Security/ClinicDoctorAccessChecker.php` | حل tenant پزشکِ مهمان |
|
||||||
|
| `src/Staff/Security/StaffRouteGuardSubscriber.php` | allowlist مسیرهای پرسنل |
|
||||||
|
| `src/Staff/Security/StaffPermissions.php` | مجوزهای پرسنل |
|
||||||
|
| `src/Patient/Security/PatientRecordScopeResolver.php` | محدودسازی دید پروندهٔ بیمار |
|
||||||
|
| `src/Treatment/Controller/TreatmentCaseController.php` | پروندهٔ درمان |
|
||||||
|
| `src/Treatment/Controller/TreatmentProtocolController.php` | پروتکل سرویس |
|
||||||
|
| `src/Treatment/Controller/SessionExecutionController.php` | اجرای جلسه توسط پرسنل |
|
||||||
|
| `src/Resource/Controller/ResourcePermissionTrait.php` | گیت منابع/دستگاهها |
|
||||||
|
| `src/Shared/Tenant/TenantFilter.php` | فیلتر Doctrine جداسازی محیط |
|
||||||
|
| `src/Shared/Tenant/TenantOwnershipChecker.php` | بررسی مالکیت محیط |
|
||||||
|
| `src/Shared/Tenant/GlobalTables.php` | entityهای عمداً غیر-tenant |
|
||||||
|
| `config/packages/security.yaml` | firewall و مسیرهای public |
|
||||||
|
| `assets/admin/` | پنل ادمین؛ مجوزهای UI |
|
||||||
|
| `docs/architecture/tenancy.md` | «چه تضمین میدهد و چه نمیدهد» |
|
||||||
|
|
||||||
|
## وضعیت فعلی
|
||||||
|
|
||||||
|
### ۱. عدم تقارن گیت در دامنهٔ Treatment — قویترین lead
|
||||||
|
|
||||||
|
`TreatmentCaseController` کلاسش فقط با احراز هویت گارد شده و مجوز را متدبهمتد میسنجد:
|
||||||
|
|
||||||
|
```php
|
||||||
|
// src/Treatment/Controller/TreatmentCaseController.php:31
|
||||||
|
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
||||||
|
class TreatmentCaseController extends BaseController
|
||||||
|
{
|
||||||
|
#[Route('/api/v1/treatment-case/{uuid}', name: 'treatment_case_show', methods: ['GET'])]
|
||||||
|
public function show(#[CurrentUser] User $user, string $uuid): JsonResponse
|
||||||
|
{
|
||||||
|
$this->denyUnlessGranted($user, 'view');
|
||||||
|
|
||||||
|
$case = $this->requireCase($user, $uuid);
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/api/v1/treatment-case/{uuid}', name: 'treatment_case_update', methods: ['PATCH'])]
|
||||||
|
public function update(#[CurrentUser] User $user, string $uuid, Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$this->denyUnlessGranted($user, 'update');
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
ولی `TreatmentProtocolController` **هیچ** `denyUnlessGranted` ندارد — نه روی خواندن، نه روی
|
||||||
|
`PUT`، نه روی `DELETE`:
|
||||||
|
|
||||||
|
```php
|
||||||
|
// src/Treatment/Controller/TreatmentProtocolController.php:24
|
||||||
|
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
||||||
|
class TreatmentProtocolController extends BaseController
|
||||||
|
{
|
||||||
|
#[Route('/api/v1/service-item/{uuid}/treatment-protocol', methods: ['PUT'])]
|
||||||
|
public function replace(#[CurrentUser] User $user, string $uuid, Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$data = json_decode($request->getContent(), true);
|
||||||
|
|
||||||
|
if (!is_array($data)) {
|
||||||
|
return $this->error(ErrorCodes::ERR_VALIDATION_001, 'بدنهٔ درخواست نامعتبر است', 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
$protocol = $this->writer->replace($user, $this->requireItem($user, $uuid), $data);
|
||||||
|
|
||||||
|
return $this->success($protocol->toArray());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
یعنی تنها دفاع، `requireItem($user, $uuid)` است. اگر آن فقط مالکیت tenant را بسنجد و نه مجوز
|
||||||
|
اکشن، هر کاربرِ داخل همان کلینیک — از جمله منشیای که مجوز `services` ندارد — میتواند پروتکل
|
||||||
|
درمان یک سرویس را بازنویسی یا حذف کند. **باید بازتولید شود، نه فرض.**
|
||||||
|
|
||||||
|
### ۲. گاردِ پرسنل مبتنی بر allowlist مسیر
|
||||||
|
|
||||||
|
```php
|
||||||
|
// src/Staff/Security/StaffRouteGuardSubscriber.php
|
||||||
|
private const ALLOWED_PREFIXES = [
|
||||||
|
'/api/v1/dashboard/staff',
|
||||||
|
'/api/v1/auth/switch-context',
|
||||||
|
'/api/v1/user/change-password',
|
||||||
|
];
|
||||||
|
|
||||||
|
private const OVERRIDING_ROLES = [
|
||||||
|
'ROLE_ADMIN', 'ROLE_CLINIC', 'ROLE_DOCTOR', 'ROLE_SECRETARY', 'ROLE_REPRESENTATION',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function onKernelRequest(RequestEvent $event): void
|
||||||
|
{
|
||||||
|
// ...
|
||||||
|
$path = $event->getRequest()->getPathInfo();
|
||||||
|
if (!str_starts_with($path, '/api/')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// ...
|
||||||
|
foreach (self::ALLOWED_PREFIXES as $prefix) {
|
||||||
|
if (str_starts_with($path, $prefix)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new AppException(ErrorCodes::ERR_FORBIDDEN_001, 'دسترسی پرسنل به این بخش مجاز نیست', 403);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
دو ریسک ساختاری که باید تست شوند:
|
||||||
|
|
||||||
|
- گارد روی `getPathInfo()` و `str_starts_with` کار میکند. مسیر نرمالنشده
|
||||||
|
(`/api/v1/../v1/patients`، دابلاسلش، درصد-انکود) ممکن است هم از شرط `/api/` رد شود هم از
|
||||||
|
allowlist بیفتد یا برعکس، از روتر عبور کند ولی از گارد نه.
|
||||||
|
- `OVERRIDING_ROLES` گارد را برای کاربر چندنقشی **کاملاً** کنار میگذارد. اگر کاربری هم پرسنل
|
||||||
|
و هم منشی باشد و context فعالش پرسنل باشد، این گارد اجرا نمیشود.
|
||||||
|
|
||||||
|
### ۳. یافتههای باز از ۲۰۲۶-۰۷-۱۹
|
||||||
|
|
||||||
|
| # | یافته | شدت | وضعیت ثبتشده |
|
||||||
|
|---|-------|-----|----------------|
|
||||||
|
| 2 | `lodash` code injection via `_.template` | 🟧 High | نیمهرفع |
|
||||||
|
| 2b | ۶۱ moderate در CKEditor build-classic (deprecated) | 🟨 Medium | باز — خارج از محدودهٔ این پرامپت |
|
||||||
|
| 5 | پسورد sandbox درگاه ملت هاردکد در `MellatGateway.php:23` | ⬜ Info | باز |
|
||||||
|
|
||||||
|
### ۴. وضعیت اکانتهای تست
|
||||||
|
|
||||||
|
`TEST_USERS.md` بیاعتبار است و `create_test_users.php` که به آن ارجاع میدهد در repo نیست.
|
||||||
|
|
||||||
|
**وضعیت واقعی DB لوکال، سنجیدهشده در ۲۰۲۶-۰۸-۰۷** — نه از حافظه، خروجی
|
||||||
|
`driver.mjs roles` و `ddev mysql`:
|
||||||
|
|
||||||
|
```
|
||||||
|
admin 09120671756 ROLE_USER,ROLE_ADMIN ✓ login موفق
|
||||||
|
representation 09124000001 ROLE_USER,ROLE_REPRESENTATION ✓ login موفق
|
||||||
|
doctor 09390039833 ROLE_USER,ROLE_CLINIC ⚠ نقشش عوض شده — دیگر DOCTOR نیست
|
||||||
|
clinic 09127000000 — ✗ کاربر در DB نیست
|
||||||
|
secretary 09123456778 — ✗ کاربر در DB نیست
|
||||||
|
```
|
||||||
|
|
||||||
|
DB از زمان آدیت قبلی دوباره seed شده. کاربران قابل استفاده که واقعاً وجود دارند:
|
||||||
|
|
||||||
|
```
|
||||||
|
id=47 09128726723 ROLE_USER,ROLE_STAFF ← کاربرِ «فقط پرسنل»، موجود است
|
||||||
|
id=11 0912000201 ROLE_USER,ROLE_DOCTOR,ROLE_CLINIC ← کاربر چندنقشی، موجود است
|
||||||
|
id=24 0912000301 ROLE_USER,ROLE_CLINIC
|
||||||
|
id=5 0912000109 ROLE_USER,ROLE_SECRETARY
|
||||||
|
id=15 0912000209 ROLE_USER,ROLE_SECRETARY
|
||||||
|
id=28 0912000309 ROLE_USER,ROLE_SECRETARY
|
||||||
|
```
|
||||||
|
|
||||||
|
پس کاربر پرسنل و کاربر چندنقشی **ساخته نمیشوند** — فقط پسوردشان باید معلوم/ست شود.
|
||||||
|
پسورد سری `0912000xxx` نامعلوم است؛ اول تلاش، بعد در صورت نیاز ست کردن هش.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## وظایف
|
||||||
|
|
||||||
|
### ۱. آمادهسازی ماتریس نقشها
|
||||||
|
|
||||||
|
اول وضعیت واقعی اکانتها را بسنج، بعد کمبود را بساز:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ddev start
|
||||||
|
node .claude/skills/qa-clinicpro/driver.mjs roles
|
||||||
|
```
|
||||||
|
|
||||||
|
کاربر «فقط پرسنل» (`09128726723`) و کاربر چندنقشی (`0912000201`) از قبل در DB هستند — ساخته
|
||||||
|
نمیشوند. فقط پسوردشان باید معلوم شود. اگر پسورد نامعلوم بود، هش را ست کن:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ddev exec php bin/console security:hash-password 'QaTest@1234'
|
||||||
|
ddev mysql -e "UPDATE users SET password_hash='<hash>' WHERE mobile_number='09128726723';"
|
||||||
|
```
|
||||||
|
|
||||||
|
این تغییر فقط روی DB لوکال است و در گزارش ثبت میشود.
|
||||||
|
|
||||||
|
علاوه بر آن، برای تست IDOR لازم است:
|
||||||
|
|
||||||
|
- **tenant دوم** (کلینیک B) با حداقل یک پروندهٔ درمان، یک سرویس با پروتکل، و یک بیمار.
|
||||||
|
اول بگرد ببین از قبل هست؛ فقط اگر نبود بساز.
|
||||||
|
|
||||||
|
خروجی این وظیفه یک جدول creds در ابتدای گزارش است.
|
||||||
|
|
||||||
|
**نحوه تست:** برای هر کاربر ساختهشده، `POST /api/v1/auth/login` باید 200 و توکن بدهد؛ در
|
||||||
|
گزارش، `roles` هر توکن decodeشده ثبت شود.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ۲. اجرای درایور بهعنوان baseline
|
||||||
|
|
||||||
|
**با درایور شروع کن، نه با grep دستی.** اسکیل `symfony-security-audit` را طبق
|
||||||
|
`.claude/skills/symfony-security-audit/SKILL.md` اجرا کن (white-box: deps/sinks/guards/secrets/config —
|
||||||
|
black-box: authz/headers/cors/injection).
|
||||||
|
|
||||||
|
خروجی خام درایور **یافته نیست، lead است**. هر lead باید با خواندن کد تأیید یا رد شود، و
|
||||||
|
leadهای ردشده با دلیل در بخش «رد شد» گزارش بیایند — دقیقاً همان قانونی که آدیت ۲۰۲۶-۰۷-۱۹
|
||||||
|
رعایت کرده بود.
|
||||||
|
|
||||||
|
**نحوه تست:** خروجی درایور با تعداد lead به تفکیک شدت در گزارش ثبت شود.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ۳. regression یافتههای آدیت قبلی
|
||||||
|
|
||||||
|
برای هر یافتهٔ `docs/security/AUDIT-2026-07-19.md` که «رفع شد» علامت خورده، بازتولیدِ همان
|
||||||
|
گزارش را دوباره اجرا کن و ثابت کن هنوز بسته است:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# CSP روی SPA ادمین
|
||||||
|
curl -sI https://clinic-pro.ddev.site/admin | grep -i content-security-policy
|
||||||
|
|
||||||
|
# APP_SECRET در فایل env تحت git
|
||||||
|
git ls-files | grep -E '^\.env' | xargs grep -nE 'APP_SECRET='
|
||||||
|
|
||||||
|
# فلگهای session cookie
|
||||||
|
ddev exec php bin/console debug:config framework session
|
||||||
|
|
||||||
|
# وابستگیهای npm
|
||||||
|
npm audit --json | python3 -c "import json,sys; m=json.load(sys.stdin)['metadata']['vulnerabilities']; print(m)"
|
||||||
|
```
|
||||||
|
|
||||||
|
هر کدام برگشته بود، **رگرسیون** است و شدتش یک درجه بالاتر ثبت میشود — چون قبلاً رفع شده بوده
|
||||||
|
و دوباره شکسته.
|
||||||
|
|
||||||
|
**نحوه تست:** جدول «یافتهٔ قبلی / وضعیت امروز / خروجی بازتولید» در گزارش.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ۴. ماتریس enforcement مجوزها — هستهٔ این آدیت
|
||||||
|
|
||||||
|
`PermissionCatalog::RESOURCES` منبع واحد است. برای **هر** جفت `(resource, action)` در آن، این
|
||||||
|
سه سؤال جواب داده شود:
|
||||||
|
|
||||||
|
1. کدام اندپوینت(ها) این مجوز را نمایندگی میکنند؟
|
||||||
|
2. آیا backend واقعاً enforce میکند، یا فقط UI دکمه را پنهان میکند؟
|
||||||
|
3. آیا منشی و پزشکِ مهمان **هر دو** enforce میشوند، یا فقط یکی؟
|
||||||
|
|
||||||
|
روش: با اکانت منشی، مجوز X را در DB خاموش کن، بعد اندپوینت متناظر را مستقیم صدا بزن.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
TOKEN=$(curl -s -X POST https://clinic-pro.ddev.site/api/v1/auth/login \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d '{"mobile":"09123456778","password":"QaTest@1234"}' | python3 -c 'import json,sys; print(json.load(sys.stdin)["data"]["token"])')
|
||||||
|
|
||||||
|
# مجوز treatment.update خاموش است → باید 403 بدهد
|
||||||
|
curl -s -o /dev/null -w '%{http_code}\n' -X PATCH \
|
||||||
|
https://clinic-pro.ddev.site/api/v1/treatment-case/<uuid> \
|
||||||
|
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
|
||||||
|
-d '{"status":"closed"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
هر جفتی که **200** بدهد یک یافتهٔ 🟧 High است (bypass مجوز)، مگر اینکه با خواندن کد ثابت شود
|
||||||
|
عمداً باز است — که آنوقت باید در `docs/api/` مستند باشد.
|
||||||
|
|
||||||
|
خروجی: جدول کامل `resource × action × role × HTTP status` در گزارش.
|
||||||
|
|
||||||
|
**نحوه تست:** خودِ جدول تست است. هر ردیف باید دستور و کد وضعیت واقعی داشته باشد.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ۵. آدیت دامنهٔ Treatment — IDOR و عدم تقارن گیت
|
||||||
|
|
||||||
|
سه کنترلر `src/Treatment/Controller/*` کامل خوانده شوند. مشخصاً:
|
||||||
|
|
||||||
|
الف) **`TreatmentProtocolController` بدون `denyUnlessGranted`** (بخش «وضعیت فعلی ۱»). بررسی کن
|
||||||
|
`requireItem()` دقیقاً چه میسنجد — فقط tenant، یا مجوز اکشن هم؟
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# با توکن منشیِ بدون مجوز services
|
||||||
|
curl -s -o /dev/null -w '%{http_code}\n' -X DELETE \
|
||||||
|
https://clinic-pro.ddev.site/api/v1/service-item/<uuid>/treatment-protocol \
|
||||||
|
-H "Authorization: Bearer $SECRETARY_TOKEN"
|
||||||
|
# انتظار: 403 — اگر 200/204 داد، یافتهٔ High
|
||||||
|
```
|
||||||
|
|
||||||
|
ب) **IDOR بین tenant.** با توکن کلینیک A، uuid منابع کلینیک B را صدا بزن. برای هر مسیر:
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /api/v1/treatment-case/{uuid}
|
||||||
|
PATCH /api/v1/treatment-case/{uuid}
|
||||||
|
GET /api/v1/treatment-case/{uuid}/plan
|
||||||
|
GET /api/v1/treatment-session/{uuid}
|
||||||
|
GET /api/v1/treatment-session/{uuid}/slot-suggestions
|
||||||
|
GET /api/v1/service-item/{uuid}/treatment-protocol
|
||||||
|
PUT /api/v1/service-item/{uuid}/treatment-protocol
|
||||||
|
DELETE /api/v1/service-item/{uuid}/treatment-protocol
|
||||||
|
POST /api/v1/dashboard/staff/treatment-session/{uuid}/start
|
||||||
|
POST /api/v1/dashboard/staff/treatment-session/{uuid}/finish
|
||||||
|
POST /api/v1/dashboard/staff/session-area/{uuid}/start
|
||||||
|
POST /api/v1/dashboard/staff/session-area/{uuid}/complete
|
||||||
|
POST /api/v1/dashboard/staff/session-area/{uuid}/skip
|
||||||
|
POST /api/v1/dashboard/staff/session-area/{uuid}/reopen
|
||||||
|
```
|
||||||
|
|
||||||
|
انتظار: 403 یا 404. هر 200 یا 500 یافته است.
|
||||||
|
|
||||||
|
ج) **پرسنلِ کلینیک A روی جلسهٔ کلینیک B.** `SessionExecutionController` با `ROLE_STAFF` گارد
|
||||||
|
شده، ولی نقش ≠ مالکیت. تست کن که `start`/`finish` روی جلسهٔ tenant دیگر رد میشود.
|
||||||
|
|
||||||
|
د) **دستکاری وضعیت.** `finish` روی جلسهای که `start` نشده، `reopen` روی ناحیهٔ جلسهٔ بسته،
|
||||||
|
`complete` دو بار پشت سر هم. اگر منجر به وضعیت ناسازگار یا 500 شود، ثبت شود.
|
||||||
|
|
||||||
|
**نحوه تست:** هر خط بالا با `curl` و کد وضعیت واقعی. برای هر یافتهٔ تأییدشده، یک تست PHPUnit
|
||||||
|
در `tests/` که آن مسیر را با کاربر بیگانه میزند و 403 انتظار دارد.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ۶. آدیت فرار از tenant
|
||||||
|
|
||||||
|
`docs/architecture/tenancy.md` صریح میگوید `TenantFilter` یک تور ایمنی است، نه authorization،
|
||||||
|
و روی سه چیز اعمال **نمیشود**: SQL خام، `getReference()`، و فرزندان aggregate.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# SQL خام
|
||||||
|
grep -rnE "createNativeQuery|->getConnection\(\)|executeQuery\(|executeStatement\(" src/ --include=*.php
|
||||||
|
|
||||||
|
# getReference
|
||||||
|
grep -rn "getReference(" src/ --include=*.php
|
||||||
|
|
||||||
|
# entityهای بدون طبقهبندی tenant
|
||||||
|
ddev exec php bin/phpunit --filter TenantSchemaCoverageTest
|
||||||
|
```
|
||||||
|
|
||||||
|
هر hit را بخوان و جواب بده: ورودی کاربر مستقیم وارد کوئری میشود؟ tenant دستی چک شده؟
|
||||||
|
|
||||||
|
**نحوه تست:** `TenantSchemaCoverageTest` سبز باشد. برای هر SQL خامی که ورودی کاربر میگیرد،
|
||||||
|
یک تست تزریق با payload واقعی (`' OR '1'='1`, `1; DROP`) و تأیید اینکه پارامتریسازی شده.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ۷. آدیت گاردِ پرسنل
|
||||||
|
|
||||||
|
الف) **نرمالسازی مسیر** (بخش «وضعیت فعلی ۲»):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
for P in \
|
||||||
|
'/api/v1/patients' \
|
||||||
|
'/api//v1/patients' \
|
||||||
|
'/api/v1/dashboard/staff/../../patients' \
|
||||||
|
'/api/v1/%2e%2e/v1/patients' \
|
||||||
|
'/API/v1/patients' ; do
|
||||||
|
printf '%s -> ' "$P"
|
||||||
|
curl -s -o /dev/null -w '%{http_code}\n' --path-as-is \
|
||||||
|
"https://clinic-pro.ddev.site$P" -H "Authorization: Bearer $STAFF_TOKEN"
|
||||||
|
done
|
||||||
|
```
|
||||||
|
|
||||||
|
هر چیزی جز 403/404 روی این مسیرها یافتهٔ 🟧 High است.
|
||||||
|
|
||||||
|
ب) **کشف مسیرهای تازهای که گارد نمیبیند.** فهرست کامل روتها را بگیر و همه را با توکن پرسنل
|
||||||
|
بزن:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ddev exec php bin/console debug:router --format=json > /tmp/routes.json
|
||||||
|
```
|
||||||
|
|
||||||
|
اندازهٔ واقعی (سنجیدهشده در ۲۰۲۶-۰۸-۰۷): **۴۹۳** روت زیر `/api/`، از این تعداد **۲۲۲** روت
|
||||||
|
`GET` و **۱۲۸** روت `GET` بدون path parameter.
|
||||||
|
|
||||||
|
**سقف این وظیفه همان ۱۲۸ روتِ بدون پارامتر است** — چون روت پارامتردار به uuid معتبر نیاز دارد و
|
||||||
|
404 آن با 403 قابل تفکیک نیست. روتهای پارامتردار در وظیفهٔ ۵ (IDOR) با uuid واقعی پوشش داده
|
||||||
|
میشوند. اگر به هر دلیل کمتر از ۱۲۸ روت زده شد، تعداد و دلیلش در گزارش بیاید — سکوت ممنوع.
|
||||||
|
|
||||||
|
برای هر روت درخواست بزن و کد وضعیت را ثبت کن. هر 200 خارج از allowlist یافته است.
|
||||||
|
|
||||||
|
ج) **کاربر چندنقشی.** با کاربر `staff + secretary`: بعد از `switch-context` به پرسنل، آیا هنوز
|
||||||
|
به مسیرهای منشی دسترسی دارد؟ اگر بله، تصمیم بگیر این طراحی است یا نشت — و در گزارش با دلیل
|
||||||
|
بنویس. اگر نشت است، گارد باید به **context فعال** نگاه کند نه صرفاً به مجموعهٔ نقشها.
|
||||||
|
|
||||||
|
**نحوه تست:** جدول `مسیر → کد وضعیت` برای هر سه بخش.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ۸. آدیت پنل ادمین
|
||||||
|
|
||||||
|
مجوزهای UI نباید تنها لایهٔ دفاع باشند. برای هر جایی که `assets/admin/` بر اساس مجوز چیزی را
|
||||||
|
پنهان میکند، تأیید کن endpoint متناظر هم بسته است — نتیجهٔ وظیفهٔ ۴ همین را میدهد؛ اینجا فقط
|
||||||
|
نگاشت UI به endpoint ثبت شود.
|
||||||
|
|
||||||
|
علاوه بر آن:
|
||||||
|
|
||||||
|
- توکن JWT در `localStorage['clinicpro-auth']` است. تأیید کن هیچ مسیر جدیدی HTML کاربرساخته را
|
||||||
|
بدون sanitize رندر نمیکند (`dangerouslySetInnerHTML`).
|
||||||
|
- CSP روی `/admin` هنوز فعال است (وظیفهٔ ۳) و صفحات جدید (treatment، staff، permissions) خطای
|
||||||
|
CSP در کنسول نمیدهند.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
grep -rn "dangerouslySetInnerHTML" assets/admin/
|
||||||
|
```
|
||||||
|
|
||||||
|
**نحوه تست:** لود هر صفحهٔ جدید پنل و ثبت خطاهای کنسول؛ اگر درایور `qa-clinicpro` این را
|
||||||
|
میدهد، از همان استفاده کن.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ۹. رفع
|
||||||
|
|
||||||
|
طبق سیاست تعیینشده:
|
||||||
|
|
||||||
|
- **Critical/High:** همان جلسه رفع + تست رگرسیون در `tests/`. هر رفع کوچک و جدا باشد، نه یک دیف
|
||||||
|
بزرگ.
|
||||||
|
- **Medium و پایینتر:** فهرست پیشنهاد با دیف پیشنهادی به کاربر نشان بده و **منتظر تأیید بمان**.
|
||||||
|
|
||||||
|
قواعد پروژه هنگام رفع:
|
||||||
|
|
||||||
|
- گیت جدید در همان لایهای که بقیه هستند — `denyUnlessGranted` در کنترلر یا checker موجود، نه
|
||||||
|
یک مکانیزم موازی جدید.
|
||||||
|
- منطق در Service، کوئری در Repository، کنترلر نازک. وابستگی با constructor injection.
|
||||||
|
- خطا با `AppException(ErrorCodes::ERR_XXX, null, $status)` و پیام فارسی از
|
||||||
|
`src/Shared/Constant/ErrorCodes.php`. کد جدید لازم شد، همانجا اضافه شود.
|
||||||
|
- اگر Entity عوض شد: `doctrine:migrations:diff` سپس `migrate`.
|
||||||
|
|
||||||
|
**نحوه تست:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ddev exec php bin/phpunit
|
||||||
|
ddev exec php vendor/bin/phpstan analyse
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ۱۰. گزارش
|
||||||
|
|
||||||
|
فایل `docs/security/AUDIT-2026-08-07.md` با همان ساختار گزارش ۲۰۲۶-۰۷-۱۹:
|
||||||
|
|
||||||
|
1. خلاصهٔ وضعیت — جدول `# / یافته / شدت / وضعیت`
|
||||||
|
2. جدول creds نقشها که آدیت با آن اجرا شد
|
||||||
|
3. برای هر یافته: فایل و خط، ریسک، شرح، **بازتولید با خروجی واقعی**، رفع اعمالشده، علت انتخاب
|
||||||
|
راهحل
|
||||||
|
4. جدول کامل ماتریس مجوز (وظیفهٔ ۴)
|
||||||
|
5. «آنچه سالم بود» — چیزهایی که تست شدند و مشکلی نداشتند
|
||||||
|
6. «leadهایی که رد شدند» با دلیل
|
||||||
|
7. «محدودیت پوشش» — صادقانه، چه چیزی تست نشد و چرا
|
||||||
|
8. «risk پذیرفتهشده» — مهاجرت CKEditor، با ارجاع به تصمیم امروز
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## نکات مهم
|
||||||
|
|
||||||
|
- **قانون گزارش:** هیچ یافتهای بدون بازتولیدِ اجراشده ثبت نمیشود. خروجی درایور lead است، نه
|
||||||
|
یافته. این قانون از گزارش قبلی میآید و باید حفظ شود.
|
||||||
|
- **دلتا یعنی دلتا.** آدیت کامل OWASP از صفر تکرار نشود. اگر یک ناحیه در ۲۰۲۶-۰۷-۱۹ سبز بود و
|
||||||
|
کدش عوض نشده، فقط اشاره شود که regression شد؛ عمیق نشو.
|
||||||
|
- **نقش ≠ مالکیت.** `#[IsGranted('ROLE_STAFF')]` فقط میگوید کاربر پرسنل است، نه اینکه این جلسه
|
||||||
|
مال اوست. هر جا فقط نقش چک شده و مالکیت نه، یک lead است.
|
||||||
|
- **`TenantFilter` را authorization فرض نکن.** روی SQL خام، `getReference()` و فرزندان aggregate
|
||||||
|
اعمال نمیشود. `docs/architecture/tenancy.md` مرجع است.
|
||||||
|
- **پیام خطا نشت ندهد.** روی منبع tenant دیگر، تفاوت پیام «یافت نشد» و «دسترسی ندارید» خودش
|
||||||
|
یک enumeration oracle است. رفتار فعلی را ثبت کن؛ اگر ناسازگار بود، یکدست کن.
|
||||||
|
- **رفع نباید رفتار مجاز را بشکند.** قبل از هر گیت جدید، مسیر مجازِ همان اندپوینت با نقش درست
|
||||||
|
تست شود که هنوز 200 میدهد.
|
||||||
|
- **`docs/api/`** — هر تغییر در قرارداد، وضعیت یا مجوز یک endpoint، همان جلسه در فایل متناظر
|
||||||
|
ثبت شود. این قانون ایستادهٔ پروژه است.
|
||||||
|
- **`nobat724_front`** کلاینت همین API است. اگر گیتی روی endpointی اضافه شد که سایت عمومی صرف
|
||||||
|
میکند، در گزارش هشدار بده — build کلاینت خطا نمیدهد.
|
||||||
|
- **بدون DoS.** آدیت روی محیط لوکال ddev اجرا میشود. تست rate limit با چند درخواست شمارشی، نه
|
||||||
|
با سیل ترافیک.
|
||||||
|
- **secret واقعی در گزارش ننویس.** مقدار را ماسک کن و فقط فایل و خط را بده.
|
||||||
@@ -33,12 +33,17 @@ const PORT = Number(process.env.CDP_PORT ?? 9444);
|
|||||||
* provisioned by SKILL.md § Phase 0 and report `✗` from `driver.mjs roles`
|
* provisioned by SKILL.md § Phase 0 and report `✗` from `driver.mjs roles`
|
||||||
* until they are. TEST_USERS.md is stale — its accounts do not exist.
|
* until they are. TEST_USERS.md is stale — its accounts do not exist.
|
||||||
*/
|
*/
|
||||||
|
// Re-verified 2026-08-07 by real logins: the DB was reseeded, so the old clinic
|
||||||
|
// and secretary numbers are gone and 09390039833 is now ROLE_CLINIC, not doctor.
|
||||||
|
// `staff` was seeded with its mobile as the password rather than QaTest@1234.
|
||||||
const ROLES = {
|
const ROLES = {
|
||||||
admin: ['09120671756', 'QaTest@1234'],
|
admin: ['09120671756', 'QaTest@1234'],
|
||||||
clinic: ['09127000000', 'QaTest@1234'],
|
clinic: ['09390039833', 'QaTest@1234'],
|
||||||
secretary: ['09123456778', 'QaTest@1234'],
|
secretary: ['0912000109', 'QaTest@1234'],
|
||||||
doctor: ['09390039833', 'QaTest@1234'],
|
doctor: ['0912000101', 'QaTest@1234'],
|
||||||
representation: ['09124000001', 'QaTest@1234'],
|
representation: ['09124000001', 'QaTest@1234'],
|
||||||
|
staff: ['09128726723', '09128726723'],
|
||||||
|
multirole: ['0912000201', 'QaTest@1234'],
|
||||||
|
|
||||||
// Provisioned by Phase 0. Reserved QA range 0912900000x, password QaTest@1234.
|
// Provisioned by Phase 0. Reserved QA range 0912900000x, password QaTest@1234.
|
||||||
doctor_solo: ['09129000001', 'QaTest@1234'], // own office, no clinic
|
doctor_solo: ['09129000001', 'QaTest@1234'], // own office, no clinic
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
# define your env variables for the test env here
|
# define your env variables for the test env here
|
||||||
KERNEL_CLASS='App\Kernel'
|
KERNEL_CLASS='App\Kernel'
|
||||||
APP_SECRET='$ecretf0rt3st'
|
# مقدارِ ثابت و آشکارا غیرعملیاتی: محیط تست هیچوقت به داده یا کاربر واقعی وصل
|
||||||
|
# نمیشود، و همین شفافیت جلوی این را میگیرد که کسی این فایل را منبع یک secret
|
||||||
|
# واقعی بپندارد. secret واقعی فقط در `.env.local` و خارج از git است.
|
||||||
|
APP_SECRET='not-a-secret-test-env-only'
|
||||||
|
|
||||||
# Test DB: doctrine's when@test config appends the `_test` suffix (see
|
# Test DB: doctrine's when@test config appends the `_test` suffix (see
|
||||||
# config/packages/doctrine.yaml), so this base name `db` becomes `db_test`.
|
# config/packages/doctrine.yaml), so this base name `db` becomes `db_test`.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { Routes, Route, Navigate, useLocation, useParams } from 'react-router-dom';
|
import { Routes, Route, Navigate, useLocation, useParams } from 'react-router';
|
||||||
import { useAuthStore } from './stores/authStore';
|
import { useAuthStore } from './stores/authStore';
|
||||||
import { usePermissions } from './hooks/usePermissions';
|
import { usePermissions } from './hooks/usePermissions';
|
||||||
import AdminLayout from './components/layout/AdminLayout';
|
import AdminLayout from './components/layout/AdminLayout';
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ const patch = api.patch as ReturnType<typeof vi.fn>;
|
|||||||
const post = api.post as ReturnType<typeof vi.fn>;
|
const post = api.post as ReturnType<typeof vi.fn>;
|
||||||
|
|
||||||
const navigate = vi.fn();
|
const navigate = vi.fn();
|
||||||
vi.mock('react-router-dom', async () => ({
|
vi.mock('react-router', async () => ({
|
||||||
...(await vi.importActual<typeof import('react-router-dom')>('react-router-dom')),
|
...(await vi.importActual<typeof import('react-router')>('react-router')),
|
||||||
useNavigate: () => navigate,
|
useNavigate: () => navigate,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import {
|
|||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import React, { useEffect, useMemo, useRef, useState } from "react";
|
import React, { useEffect, useMemo, useRef, useState } from "react";
|
||||||
import ReactDOM from "react-dom";
|
import ReactDOM from "react-dom";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import type { ApiResponse } from "../lib/api";
|
import type { ApiResponse } from "../lib/api";
|
||||||
import { api } from "../lib/api";
|
import { api } from "../lib/api";
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useState, useMemo } from 'react';
|
import { useState, useMemo } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router';
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import {
|
import {
|
||||||
TrashIcon, EnvelopeIcon, ArrowPathIcon, NoSymbolIcon, EyeIcon, ShieldCheckIcon,
|
TrashIcon, EnvelopeIcon, ArrowPathIcon, NoSymbolIcon, EyeIcon, ShieldCheckIcon,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router';
|
||||||
import { api } from '../lib/api';
|
import { api } from '../lib/api';
|
||||||
import type { ApiResponse } from '../lib/api';
|
import type { ApiResponse } from '../lib/api';
|
||||||
import { useAuthStore } from '../stores/authStore';
|
import { useAuthStore } from '../stores/authStore';
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useEffect, useMemo, useState } from 'react';
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router';
|
||||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import SearchableSelect from './ui/SearchableSelect';
|
import SearchableSelect from './ui/SearchableSelect';
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
* and the parent passes the query key to invalidate.
|
* and the parent passes the query key to invalidate.
|
||||||
*/
|
*/
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Link, useNavigate } from 'react-router-dom';
|
import { Link, useNavigate } from 'react-router';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import AppointmentStatusDropdown, { STATUS_META } from '../ui/AppointmentStatusDropdown';
|
import AppointmentStatusDropdown, { STATUS_META } from '../ui/AppointmentStatusDropdown';
|
||||||
import { findRecordUuid } from '../AppointmentActions';
|
import { findRecordUuid } from '../AppointmentActions';
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
* DoctorDashboard feed it their (real-API) data.
|
* DoctorDashboard feed it their (real-API) data.
|
||||||
*/
|
*/
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router';
|
||||||
import { TauriStatCards, type DashboardStats } from './TauriStatCards';
|
import { TauriStatCards, type DashboardStats } from './TauriStatCards';
|
||||||
import { TauriBarChart, TauriLineChart, type ChartPoint } from './TauriCharts';
|
import { TauriBarChart, TauriLineChart, type ChartPoint } from './TauriCharts';
|
||||||
import { NewAppointmentsTable, type ApptRow } from './NewAppointmentsTable';
|
import { NewAppointmentsTable, type ApptRow } from './NewAppointmentsTable';
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router';
|
||||||
import { useHolidays } from '../../hooks/useResourceCalendar';
|
import { useHolidays } from '../../hooks/useResourceCalendar';
|
||||||
import { formatDate, currentJalaliYear } from '../../lib/utils';
|
import { formatDate, currentJalaliYear } from '../../lib/utils';
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { Outlet } from 'react-router-dom';
|
import { Outlet } from 'react-router';
|
||||||
import Sidebar from './Sidebar';
|
import Sidebar from './Sidebar';
|
||||||
import Topbar from './Topbar';
|
import Topbar from './Topbar';
|
||||||
import { useUiStore, applyBrand } from '../../stores/uiStore';
|
import { useUiStore, applyBrand } from '../../stores/uiStore';
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import { screen, fireEvent } from '@testing-library/react';
|
|||||||
import { renderWithProviders } from '../../test/utils';
|
import { renderWithProviders } from '../../test/utils';
|
||||||
|
|
||||||
const navigateMock = vi.fn();
|
const navigateMock = vi.fn();
|
||||||
vi.mock('react-router-dom', async () => {
|
vi.mock('react-router', async () => {
|
||||||
const actual = await vi.importActual<typeof import('react-router-dom')>('react-router-dom');
|
const actual = await vi.importActual<typeof import('react-router')>('react-router');
|
||||||
return { ...actual, useNavigate: () => navigateMock };
|
return { ...actual, useNavigate: () => navigateMock };
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useEffect, useRef, useState } from 'react';
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router';
|
||||||
import {
|
import {
|
||||||
ChevronDownIcon,
|
ChevronDownIcon,
|
||||||
UserCircleIcon,
|
UserCircleIcon,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useMemo, useState } from 'react';
|
import React, { useMemo, useState } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router';
|
||||||
import { SearchHeaderP } from '../../pages/subscriptionIcons';
|
import { SearchHeaderP } from '../../pages/subscriptionIcons';
|
||||||
import { useAuthStore } from '../../stores/authStore';
|
import { useAuthStore } from '../../stores/authStore';
|
||||||
import { usePermissions } from '../../hooks/usePermissions';
|
import { usePermissions } from '../../hooks/usePermissions';
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ import {
|
|||||||
WrenchScrewdriverIcon,
|
WrenchScrewdriverIcon,
|
||||||
} from "@heroicons/react/24/outline";
|
} from "@heroicons/react/24/outline";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { NavLink, useLocation, useNavigate } from "react-router-dom";
|
import { NavLink, useLocation, useNavigate } from "react-router";
|
||||||
import { usePermissions } from "../../hooks/usePermissions";
|
import { usePermissions } from "../../hooks/usePermissions";
|
||||||
import { useSecretaryEarnings } from "../../hooks/useSecretaryEarnings";
|
import { useSecretaryEarnings } from "../../hooks/useSecretaryEarnings";
|
||||||
import { useSubscription } from "../../hooks/useSubscription";
|
import { useSubscription } from "../../hooks/useSubscription";
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import { screen, fireEvent } from '@testing-library/react';
|
|||||||
import { renderWithProviders } from '../../test/utils';
|
import { renderWithProviders } from '../../test/utils';
|
||||||
|
|
||||||
const navigateMock = vi.fn();
|
const navigateMock = vi.fn();
|
||||||
vi.mock('react-router-dom', async () => {
|
vi.mock('react-router', async () => {
|
||||||
const actual = await vi.importActual<typeof import('react-router-dom')>('react-router-dom');
|
const actual = await vi.importActual<typeof import('react-router')>('react-router');
|
||||||
return { ...actual, useNavigate: () => navigateMock };
|
return { ...actual, useNavigate: () => navigateMock };
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router';
|
||||||
import {
|
import {
|
||||||
BellIcon, SunIcon, MoonIcon, Bars3Icon, Cog6ToothIcon,
|
BellIcon, SunIcon, MoonIcon, Bars3Icon, Cog6ToothIcon,
|
||||||
MagnifyingGlassIcon, XMarkIcon, CheckIcon,
|
MagnifyingGlassIcon, XMarkIcon, CheckIcon,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Link, useLocation } from 'react-router-dom';
|
import { Link, useLocation } from 'react-router';
|
||||||
|
|
||||||
const LINKS = [
|
const LINKS = [
|
||||||
{ to: '/admin/resources', label: 'منابع' },
|
{ to: '/admin/resources', label: 'منابع' },
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
import { screen, fireEvent, render } from '@testing-library/react';
|
import { screen, fireEvent, render } from '@testing-library/react';
|
||||||
import { MemoryRouter, Route, Routes, Link } from 'react-router-dom';
|
import { MemoryRouter, Route, Routes, Link } from 'react-router';
|
||||||
import BackButton from './BackButton';
|
import BackButton from './BackButton';
|
||||||
import PageHeader from './PageHeader';
|
import PageHeader from './PageHeader';
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router';
|
||||||
import { LockClosedIcon } from '@heroicons/react/24/outline';
|
import { LockClosedIcon } from '@heroicons/react/24/outline';
|
||||||
import { useSubscription } from '../../hooks/useSubscription';
|
import { useSubscription } from '../../hooks/useSubscription';
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router';
|
||||||
import { ChevronLeftIcon } from '@heroicons/react/24/outline';
|
import { ChevronLeftIcon } from '@heroicons/react/24/outline';
|
||||||
import BackButton from './BackButton';
|
import BackButton from './BackButton';
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { useLocation, useNavigate } from 'react-router-dom';
|
import { useLocation, useNavigate } from 'react-router';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* رفتار یکسانِ «بازگشت» در کل پنل: اگر کاربر از صفحهٔ دیگری داخل خود پنل آمده باشد،
|
* رفتار یکسانِ «بازگشت» در کل پنل: اگر کاربر از صفحهٔ دیگری داخل خود پنل آمده باشد،
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import { render, screen, fireEvent } from '@testing-library/react';
|
import { render, screen, fireEvent } from '@testing-library/react';
|
||||||
import { MemoryRouter, Route, Routes, useLocation } from 'react-router-dom';
|
import { MemoryRouter, Route, Routes, useLocation } from 'react-router';
|
||||||
import { useUrlState, pageOf } from './useUrlState';
|
import { useUrlState, pageOf } from './useUrlState';
|
||||||
import BackButton from '../components/ui/BackButton';
|
import BackButton from '../components/ui/BackButton';
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||||
import { useSearchParams } from 'react-router-dom';
|
import { useSearchParams } from 'react-router';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* وضعیت لیست (جستجو، فیلترها، صفحه، تب) در query string زندگی میکند، نه در state
|
* وضعیت لیست (جستجو، فیلترها، صفحه، تب) در query string زندگی میکند، نه در state
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { createRoot } from 'react-dom/client';
|
import { createRoot } from 'react-dom/client';
|
||||||
import { BrowserRouter } from 'react-router-dom';
|
import { BrowserRouter } from 'react-router';
|
||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
import { Toaster } from 'sonner';
|
import { Toaster } from 'sonner';
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
import { useNavigate, useSearchParams } from 'react-router';
|
||||||
import { PlusIcon, ChevronRightIcon, MagnifyingGlassIcon } from '@heroicons/react/24/outline';
|
import { PlusIcon, ChevronRightIcon, MagnifyingGlassIcon } from '@heroicons/react/24/outline';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { api } from '../lib/api';
|
import { api } from '../lib/api';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||||
import { screen, waitFor } from '@testing-library/react';
|
import { screen, waitFor } from '@testing-library/react';
|
||||||
import { Routes, Route } from 'react-router-dom';
|
import { Routes, Route } from 'react-router';
|
||||||
import { renderWithProviders } from '../test/utils';
|
import { renderWithProviders } from '../test/utils';
|
||||||
|
|
||||||
vi.mock('../lib/api', () => ({
|
vi.mock('../lib/api', () => ({
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useParams, useNavigate } from 'react-router-dom';
|
import { useParams, useNavigate } from 'react-router';
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { ArrowRightIcon } from '@heroicons/react/24/outline';
|
import { ArrowRightIcon } from '@heroicons/react/24/outline';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||||
import { screen, fireEvent, waitFor } from '@testing-library/react';
|
import { screen, fireEvent, waitFor } from '@testing-library/react';
|
||||||
import { Routes, Route } from 'react-router-dom';
|
import { Routes, Route } from 'react-router';
|
||||||
import { renderWithProviders } from '../test/utils';
|
import { renderWithProviders } from '../test/utils';
|
||||||
|
|
||||||
vi.mock('../lib/api', () => ({
|
vi.mock('../lib/api', () => ({
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||||
import { screen, fireEvent, waitFor } from '@testing-library/react';
|
import { screen, fireEvent, waitFor } from '@testing-library/react';
|
||||||
import { Routes, Route } from 'react-router-dom';
|
import { Routes, Route } from 'react-router';
|
||||||
import { renderWithProviders } from '../test/utils';
|
import { renderWithProviders } from '../test/utils';
|
||||||
|
|
||||||
vi.mock('../lib/api', () => ({
|
vi.mock('../lib/api', () => ({
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
import { useNavigate, useParams, Link } from 'react-router-dom';
|
import { useNavigate, useParams, Link } from 'react-router';
|
||||||
import { ChevronRightIcon } from '@heroicons/react/24/outline';
|
import { ChevronRightIcon } from '@heroicons/react/24/outline';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { api } from '../lib/api';
|
import { api } from '../lib/api';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useEffect, useRef, useState } from 'react';
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
|
import { Link, useNavigate, useSearchParams } from 'react-router';
|
||||||
import {
|
import {
|
||||||
PlusIcon, ChevronRightIcon, ChevronLeftIcon, ChevronDownIcon, CalendarDaysIcon,
|
PlusIcon, ChevronRightIcon, ChevronLeftIcon, ChevronDownIcon, CalendarDaysIcon,
|
||||||
AdjustmentsHorizontalIcon,
|
AdjustmentsHorizontalIcon,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||||
import { screen } from '@testing-library/react';
|
import { screen } from '@testing-library/react';
|
||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
import { Routes, Route } from 'react-router-dom';
|
import { Routes, Route } from 'react-router';
|
||||||
import { renderWithProviders } from '@/test/utils';
|
import { renderWithProviders } from '@/test/utils';
|
||||||
|
|
||||||
vi.mock('@ckeditor/ckeditor5-react', () => ({ CKEditor: () => null }));
|
vi.mock('@ckeditor/ckeditor5-react', () => ({ CKEditor: () => null }));
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useParams, useNavigate } from 'react-router-dom';
|
import { useParams, useNavigate } from 'react-router';
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { useForm, Controller } from 'react-hook-form';
|
import { useForm, Controller } from 'react-hook-form';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router';
|
||||||
import { EyeIcon, PencilIcon, TrashIcon, PlusIcon, MagnifyingGlassIcon } from '@heroicons/react/24/outline';
|
import { EyeIcon, PencilIcon, TrashIcon, PlusIcon, MagnifyingGlassIcon } from '@heroicons/react/24/outline';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { api } from '../lib/api';
|
import { api } from '../lib/api';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { useParams, useSearchParams } from 'react-router-dom';
|
import { useParams, useSearchParams } from 'react-router';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { api } from '../lib/api';
|
import { api } from '../lib/api';
|
||||||
import { formatRial, formatNumber, formatDate, formatDateTime } from '../lib/utils';
|
import { formatRial, formatNumber, formatDate, formatDateTime } from '../lib/utils';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
import { useNavigate, useSearchParams } from 'react-router';
|
||||||
import { ArrowPathIcon } from '@heroicons/react/24/outline';
|
import { ArrowPathIcon } from '@heroicons/react/24/outline';
|
||||||
import { api } from '../lib/api';
|
import { api } from '../lib/api';
|
||||||
import { formatRial, formatNumber, toGregorianDate, todayIso } from '../lib/utils';
|
import { formatRial, formatNumber, toGregorianDate, todayIso } from '../lib/utils';
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { CubeIcon, UserGroupIcon } from '@heroicons/react/24/outline';
|
import { CubeIcon, UserGroupIcon } from '@heroicons/react/24/outline';
|
||||||
import { api } from '../lib/api';
|
import { api } from '../lib/api';
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ vi.mock('../components/ClinicDoctorsManager', () => ({
|
|||||||
default: () => <div data-testid="doctors-manager" />,
|
default: () => <div data-testid="doctors-manager" />,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
import { Routes, Route } from 'react-router-dom';
|
import { Routes, Route } from 'react-router';
|
||||||
import { api } from '../lib/api';
|
import { api } from '../lib/api';
|
||||||
import { useAuthStore } from '../stores/authStore';
|
import { useAuthStore } from '../stores/authStore';
|
||||||
import ClinicDetailPage from './ClinicDetailPage';
|
import ClinicDetailPage from './ClinicDetailPage';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useState, useEffect, useMemo, useRef } from 'react';
|
import React, { useState, useEffect, useMemo, useRef } from 'react';
|
||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
import { useParams, useNavigate } from 'react-router-dom';
|
import { useParams, useNavigate } from 'react-router';
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useEffect, useMemo } from 'react';
|
import { useEffect, useMemo } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router';
|
||||||
import { PencilIcon } from '@heroicons/react/24/outline';
|
import { PencilIcon } from '@heroicons/react/24/outline';
|
||||||
import { useAuthStore } from '../stores/authStore';
|
import { useAuthStore } from '../stores/authStore';
|
||||||
import { usePermissions } from '../hooks/usePermissions';
|
import { usePermissions } from '../hooks/usePermissions';
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router';
|
||||||
import { useMutation } from '@tanstack/react-query';
|
import { useMutation } from '@tanstack/react-query';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router';
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import {
|
import {
|
||||||
PlusIcon, PencilIcon, WrenchScrewdriverIcon, BanknotesIcon,
|
PlusIcon, PencilIcon, WrenchScrewdriverIcon, BanknotesIcon,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router';
|
||||||
import {
|
import {
|
||||||
EyeIcon,
|
EyeIcon,
|
||||||
TrashIcon,
|
TrashIcon,
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ vi.mock('../lib/api', () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
const navigate = vi.fn();
|
const navigate = vi.fn();
|
||||||
vi.mock('react-router-dom', async () => ({
|
vi.mock('react-router', async () => ({
|
||||||
...(await vi.importActual<typeof import('react-router-dom')>('react-router-dom')),
|
...(await vi.importActual<typeof import('react-router')>('react-router')),
|
||||||
useNavigate: () => navigate,
|
useNavigate: () => navigate,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useMemo, useState } from 'react';
|
import React, { useMemo, useState } from 'react';
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router';
|
||||||
import {
|
import {
|
||||||
UserGroupIcon, HeartIcon, BuildingOffice2Icon, CalendarDaysIcon,
|
UserGroupIcon, HeartIcon, BuildingOffice2Icon, CalendarDaysIcon,
|
||||||
CreditCardIcon, ArrowPathIcon, BellAlertIcon, ChatBubbleLeftEllipsisIcon,
|
CreditCardIcon, ArrowPathIcon, BellAlertIcon, ChatBubbleLeftEllipsisIcon,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, { useState, useEffect, useMemo, useRef, useCallback } from 'react';
|
import React, { useState, useEffect, useMemo, useRef, useCallback } from 'react';
|
||||||
import { avatarGradient } from '../lib/avatarColors';
|
import { avatarGradient } from '../lib/avatarColors';
|
||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
import { useParams, useNavigate, useSearchParams } from 'react-router-dom';
|
import { useParams, useNavigate, useSearchParams } from 'react-router';
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { useForm, Controller } from 'react-hook-form';
|
import { useForm, Controller } from 'react-hook-form';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState, useMemo } from 'react';
|
import React, { useState, useMemo } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router';
|
||||||
import { useQuery, useMutation } from '@tanstack/react-query';
|
import { useQuery, useMutation } from '@tanstack/react-query';
|
||||||
import { useForm, Controller } from 'react-hook-form';
|
import { useForm, Controller } from 'react-hook-form';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useState, useEffect, useMemo } from 'react';
|
import React, { useState, useEffect, useMemo } from 'react';
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router';
|
||||||
import {
|
import {
|
||||||
MagnifyingGlassIcon, PlusIcon, EyeIcon, TrashIcon, ArrowPathIcon,
|
MagnifyingGlassIcon, PlusIcon, EyeIcon, TrashIcon, ArrowPathIcon,
|
||||||
CheckCircleIcon, XCircleIcon, TableCellsIcon, Squares2X2Icon, DevicePhoneMobileIcon,
|
CheckCircleIcon, XCircleIcon, TableCellsIcon, Squares2X2Icon, DevicePhoneMobileIcon,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useParams, useNavigate } from 'react-router-dom';
|
import { useParams, useNavigate } from 'react-router';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { api } from '../lib/api';
|
import { api } from '../lib/api';
|
||||||
import type { ApiResponse } from '../lib/api';
|
import type { ApiResponse } from '../lib/api';
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useEffect, useRef, useState } from 'react';
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
import { useSearchParams } from 'react-router-dom';
|
import { useSearchParams } from 'react-router';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { EyeIcon, EyeSlashIcon } from '@heroicons/react/24/outline';
|
import { EyeIcon, EyeSlashIcon } from '@heroicons/react/24/outline';
|
||||||
import { useAuthStore } from '../stores/authStore';
|
import { useAuthStore } from '../stores/authStore';
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
|||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import React, { useCallback, useRef, useState } from "react";
|
import React, { useCallback, useRef, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import FeatureGate from "../components/ui/FeatureGate";
|
import FeatureGate from "../components/ui/FeatureGate";
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||||
import { screen, fireEvent } from '@testing-library/react';
|
import { screen, fireEvent } from '@testing-library/react';
|
||||||
import { Route, Routes } from 'react-router-dom';
|
import { Route, Routes } from 'react-router';
|
||||||
import { renderWithProviders } from '../test/utils';
|
import { renderWithProviders } from '../test/utils';
|
||||||
|
|
||||||
vi.mock('sonner', () => ({ toast: { info: vi.fn(), success: vi.fn(), error: vi.fn() } }));
|
vi.mock('sonner', () => ({ toast: { info: vi.fn(), success: vi.fn(), error: vi.fn() } }));
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
import { useNavigate, useParams } from 'react-router';
|
||||||
import { ChevronRightIcon, ChevronDownIcon, PlusIcon } from '@heroicons/react/24/outline';
|
import { ChevronRightIcon, ChevronDownIcon, PlusIcon } from '@heroicons/react/24/outline';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import PageHeader from '../components/ui/PageHeader';
|
import PageHeader from '../components/ui/PageHeader';
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import { screen, fireEvent, waitFor } from '@testing-library/react';
|
|||||||
import { renderWithProviders } from '../test/utils';
|
import { renderWithProviders } from '../test/utils';
|
||||||
|
|
||||||
const navigate = vi.fn();
|
const navigate = vi.fn();
|
||||||
vi.mock('react-router-dom', async (orig) => ({
|
vi.mock('react-router', async (orig) => ({
|
||||||
...(await orig<typeof import('react-router-dom')>()),
|
...(await orig<typeof import('react-router')>()),
|
||||||
useNavigate: () => navigate,
|
useNavigate: () => navigate,
|
||||||
}));
|
}));
|
||||||
vi.mock('../lib/api', () => ({ api: { get: vi.fn() }, ApiError: class extends Error {} }));
|
vi.mock('../lib/api', () => ({ api: { get: vi.fn() }, ApiError: class extends Error {} }));
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
import { useNavigate, useSearchParams } from 'react-router';
|
||||||
import { ArrowPathIcon } from '@heroicons/react/24/outline';
|
import { ArrowPathIcon } from '@heroicons/react/24/outline';
|
||||||
import PageHeader from '../components/ui/PageHeader';
|
import PageHeader from '../components/ui/PageHeader';
|
||||||
import Pagination from '../components/ui/Pagination';
|
import Pagination from '../components/ui/Pagination';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import SettingsLayout from "../components/layout/SettingsLayout";
|
import SettingsLayout from "../components/layout/SettingsLayout";
|
||||||
import ConfirmDialog from "../components/ui/ConfirmDialog";
|
import ConfirmDialog from "../components/ui/ConfirmDialog";
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||||
import { screen, fireEvent, waitFor } from '@testing-library/react';
|
import { screen, fireEvent, waitFor } from '@testing-library/react';
|
||||||
import { Routes, Route } from 'react-router-dom';
|
import { Routes, Route } from 'react-router';
|
||||||
import { renderWithProviders } from '../test/utils';
|
import { renderWithProviders } from '../test/utils';
|
||||||
|
|
||||||
vi.mock('sonner', () => ({ toast: { success: vi.fn(), error: vi.fn() } }));
|
vi.mock('sonner', () => ({ toast: { success: vi.fn(), error: vi.fn() } }));
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useParams, useNavigate } from 'react-router-dom';
|
import { useParams, useNavigate } from 'react-router';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { api } from '../lib/api';
|
import { api } from '../lib/api';
|
||||||
import type { ApiResponse } from '../lib/api';
|
import type { ApiResponse } from '../lib/api';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||||
import { screen, fireEvent, waitFor } from '@testing-library/react';
|
import { screen, fireEvent, waitFor } from '@testing-library/react';
|
||||||
import { Routes, Route } from 'react-router-dom';
|
import { Routes, Route } from 'react-router';
|
||||||
import { renderWithProviders } from '../test/utils';
|
import { renderWithProviders } from '../test/utils';
|
||||||
|
|
||||||
vi.mock('sonner', () => ({ toast: { success: vi.fn(), error: vi.fn() } }));
|
vi.mock('sonner', () => ({ toast: { success: vi.fn(), error: vi.fn() } }));
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { useParams, useSearchParams, Link, useNavigate } from 'react-router-dom';
|
import { useParams, useSearchParams, Link, useNavigate } from 'react-router';
|
||||||
import {
|
import {
|
||||||
ChevronRightIcon, PencilIcon, ClipboardDocumentCheckIcon, DocumentTextIcon, RectangleStackIcon,
|
ChevronRightIcon, PencilIcon, ClipboardDocumentCheckIcon, DocumentTextIcon, RectangleStackIcon,
|
||||||
ArrowPathRoundedSquareIcon,
|
ArrowPathRoundedSquareIcon,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { useForm } from 'react-hook-form';
|
|||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { useParams, useNavigate, Link } from 'react-router-dom';
|
import { useParams, useNavigate, Link } from 'react-router';
|
||||||
import { ChevronRightIcon } from '@heroicons/react/24/outline';
|
import { ChevronRightIcon } from '@heroicons/react/24/outline';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { api } from '../lib/api';
|
import { api } from '../lib/api';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { Link, useNavigate } from 'react-router-dom';
|
import { Link, useNavigate } from 'react-router';
|
||||||
import {
|
import {
|
||||||
PencilIcon, EyeIcon, ExclamationCircleIcon, IdentificationIcon,
|
PencilIcon, EyeIcon, ExclamationCircleIcon, IdentificationIcon,
|
||||||
UserIcon, EllipsisHorizontalIcon,
|
UserIcon, EllipsisHorizontalIcon,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useParams, useNavigate } from 'react-router-dom';
|
import { useParams, useNavigate } from 'react-router';
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { ArrowRightIcon } from '@heroicons/react/24/outline';
|
import { ArrowRightIcon } from '@heroicons/react/24/outline';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import { screen } from '@testing-library/react';
|
|||||||
import { renderWithProviders } from '../test/utils';
|
import { renderWithProviders } from '../test/utils';
|
||||||
|
|
||||||
const navSpy = vi.fn();
|
const navSpy = vi.fn();
|
||||||
vi.mock('react-router-dom', async (orig) => ({
|
vi.mock('react-router', async (orig) => ({
|
||||||
...(await orig<typeof import('react-router-dom')>()),
|
...(await orig<typeof import('react-router')>()),
|
||||||
useNavigate: () => navSpy,
|
useNavigate: () => navSpy,
|
||||||
}));
|
}));
|
||||||
vi.mock('sonner', () => ({ toast: { success: vi.fn(), error: vi.fn() } }));
|
vi.mock('sonner', () => ({ toast: { success: vi.fn(), error: vi.fn() } }));
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { useSearchParams, useNavigate, Link } from 'react-router-dom';
|
import { useSearchParams, useNavigate, Link } from 'react-router';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { CheckBadgeIcon, SparklesIcon, CalendarDaysIcon, CreditCardIcon, ArrowLeftIcon } from '@heroicons/react/24/outline';
|
import { CheckBadgeIcon, SparklesIcon, CalendarDaysIcon, CreditCardIcon, ArrowLeftIcon } from '@heroicons/react/24/outline';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router';
|
||||||
import {
|
import {
|
||||||
MagnifyingGlassIcon, EyeIcon,
|
MagnifyingGlassIcon, EyeIcon,
|
||||||
BanknotesIcon, ClockIcon, CreditCardIcon, ArrowDownTrayIcon,
|
BanknotesIcon, ClockIcon, CreditCardIcon, ArrowDownTrayIcon,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useParams, useNavigate } from 'react-router-dom';
|
import { useParams, useNavigate } from 'react-router';
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { useForm, Controller } from 'react-hook-form';
|
import { useForm, Controller } from 'react-hook-form';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router';
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { api } from '../lib/api';
|
import { api } from '../lib/api';
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useParams, useNavigate } from 'react-router-dom';
|
import { useParams, useNavigate } from 'react-router';
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { PencilIcon, TrashIcon, CheckCircleIcon, XCircleIcon, UserGroupIcon, CalendarDaysIcon, ClockIcon, ReceiptPercentIcon, PlusIcon } from '@heroicons/react/24/outline';
|
import { PencilIcon, TrashIcon, CheckCircleIcon, XCircleIcon, UserGroupIcon, CalendarDaysIcon, ClockIcon, ReceiptPercentIcon, PlusIcon } from '@heroicons/react/24/outline';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router';
|
||||||
import { EyeIcon, NoSymbolIcon, PlusIcon, MagnifyingGlassIcon } from '@heroicons/react/24/outline';
|
import { EyeIcon, NoSymbolIcon, PlusIcon, MagnifyingGlassIcon } from '@heroicons/react/24/outline';
|
||||||
import { useForm, Controller } from 'react-hook-form';
|
import { useForm, Controller } from 'react-hook-form';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import React, { useEffect, useRef, useState } from 'react';
|
|||||||
import { useUrlState } from '../hooks/useUrlState';
|
import { useUrlState } from '../hooks/useUrlState';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router';
|
||||||
import {
|
import {
|
||||||
PlusIcon, EllipsisHorizontalIcon, EyeIcon, PencilIcon, ArrowDownOnSquareIcon,
|
PlusIcon, EllipsisHorizontalIcon, EyeIcon, PencilIcon, ArrowDownOnSquareIcon,
|
||||||
UserCircleIcon, PhoneIcon,
|
UserCircleIcon, PhoneIcon,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useCallback, useMemo, useState } from 'react';
|
import React, { useCallback, useMemo, useState } from 'react';
|
||||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
import { useNavigate, useSearchParams } from 'react-router';
|
||||||
import PageHeader from '../components/ui/PageHeader';
|
import PageHeader from '../components/ui/PageHeader';
|
||||||
import SearchableSelect from '../components/ui/SearchableSelect';
|
import SearchableSelect from '../components/ui/SearchableSelect';
|
||||||
import HoldCountdown from '../components/HoldCountdown';
|
import HoldCountdown from '../components/HoldCountdown';
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ vi.mock('../lib/api', () => ({
|
|||||||
vi.mock('sonner', () => ({ toast: { success: vi.fn(), error: vi.fn() } }));
|
vi.mock('sonner', () => ({ toast: { success: vi.fn(), error: vi.fn() } }));
|
||||||
vi.mock('../hooks/usePermissions', () => ({ usePermissions: () => ({ can: () => true }) }));
|
vi.mock('../hooks/usePermissions', () => ({ usePermissions: () => ({ can: () => true }) }));
|
||||||
|
|
||||||
import { Routes, Route } from 'react-router-dom';
|
import { Routes, Route } from 'react-router';
|
||||||
import { api } from '../lib/api';
|
import { api } from '../lib/api';
|
||||||
import ResourceDetailPage from './ResourceDetailPage';
|
import ResourceDetailPage from './ResourceDetailPage';
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router';
|
||||||
import { PencilIcon } from '@heroicons/react/24/outline';
|
import { PencilIcon } from '@heroicons/react/24/outline';
|
||||||
import PageHeader from '../components/ui/PageHeader';
|
import PageHeader from '../components/ui/PageHeader';
|
||||||
import ConfirmDialog from '../components/ui/ConfirmDialog';
|
import ConfirmDialog from '../components/ui/ConfirmDialog';
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useMemo, useState } from 'react';
|
import React, { useMemo, useState } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router';
|
||||||
import { PlusIcon } from '@heroicons/react/24/outline';
|
import { PlusIcon } from '@heroicons/react/24/outline';
|
||||||
import PageHeader from '../components/ui/PageHeader';
|
import PageHeader from '../components/ui/PageHeader';
|
||||||
import DataTable, { type Column } from '../components/ui/DataTable';
|
import DataTable, { type Column } from '../components/ui/DataTable';
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { TrashIcon, PencilIcon, MagnifyingGlassIcon, CurrencyDollarIcon } from '@heroicons/react/24/outline';
|
import { TrashIcon, PencilIcon, MagnifyingGlassIcon, CurrencyDollarIcon } from '@heroicons/react/24/outline';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { api } from '../lib/api';
|
import { api } from '../lib/api';
|
||||||
import { useUrlState, pageOf } from '../hooks/useUrlState';
|
import { useUrlState, pageOf } from '../hooks/useUrlState';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||||
import { screen, fireEvent, waitFor } from '@testing-library/react';
|
import { screen, fireEvent, waitFor } from '@testing-library/react';
|
||||||
import { Routes, Route } from 'react-router-dom';
|
import { Routes, Route } from 'react-router';
|
||||||
import { renderWithProviders } from '../test/utils';
|
import { renderWithProviders } from '../test/utils';
|
||||||
|
|
||||||
vi.mock('../lib/api', () => ({
|
vi.mock('../lib/api', () => ({
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
import { Link, useParams } from 'react-router-dom';
|
import { Link, useParams } from 'react-router';
|
||||||
import { ChevronRightIcon } from '@heroicons/react/24/outline';
|
import { ChevronRightIcon } from '@heroicons/react/24/outline';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { api } from '../lib/api';
|
import { api } from '../lib/api';
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useAuthStore, ContextItem } from '../stores/authStore';
|
import { useAuthStore, ContextItem } from '../stores/authStore';
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||||
import { screen, fireEvent } from '@testing-library/react';
|
import { screen, fireEvent } from '@testing-library/react';
|
||||||
import { Routes, Route } from 'react-router-dom';
|
import { Routes, Route } from 'react-router';
|
||||||
import { renderWithProviders } from '../test/utils';
|
import { renderWithProviders } from '../test/utils';
|
||||||
|
|
||||||
vi.mock('sonner', () => ({ toast: { success: vi.fn(), error: vi.fn() } }));
|
vi.mock('sonner', () => ({ toast: { success: vi.fn(), error: vi.fn() } }));
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useParams, useNavigate, Link } from 'react-router-dom';
|
import { useParams, useNavigate, Link } from 'react-router';
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import {
|
import {
|
||||||
BanknotesIcon, ShieldCheckIcon, ClockIcon, UsersIcon, PencilIcon,
|
BanknotesIcon, ShieldCheckIcon, ClockIcon, UsersIcon, PencilIcon,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||||
import { screen, fireEvent, waitFor } from '@testing-library/react';
|
import { screen, fireEvent, waitFor } from '@testing-library/react';
|
||||||
import { Routes, Route } from 'react-router-dom';
|
import { Routes, Route } from 'react-router';
|
||||||
import { renderWithProviders } from '../test/utils';
|
import { renderWithProviders } from '../test/utils';
|
||||||
|
|
||||||
vi.mock('sonner', () => ({ toast: { success: vi.fn(), error: vi.fn() } }));
|
vi.mock('sonner', () => ({ toast: { success: vi.fn(), error: vi.fn() } }));
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useParams, useNavigate } from 'react-router-dom';
|
import { useParams, useNavigate } from 'react-router';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { api } from '../lib/api';
|
import { api } from '../lib/api';
|
||||||
import type { ApiResponse } from '../lib/api';
|
import type { ApiResponse } from '../lib/api';
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router';
|
||||||
import { ChevronLeftIcon } from '@heroicons/react/24/outline';
|
import { ChevronLeftIcon } from '@heroicons/react/24/outline';
|
||||||
import { menuForRole } from '../components/layout/SettingsLayout';
|
import { menuForRole } from '../components/layout/SettingsLayout';
|
||||||
import { useAuthStore } from '../stores/authStore';
|
import { useAuthStore } from '../stores/authStore';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { useParams, useNavigate } from 'react-router-dom';
|
import { useParams, useNavigate } from 'react-router';
|
||||||
import { ArrowRightIcon, CheckIcon, XMarkIcon, ArrowUpTrayIcon, DocumentTextIcon } from '@heroicons/react/24/outline';
|
import { ArrowRightIcon, CheckIcon, XMarkIcon, ArrowUpTrayIcon, DocumentTextIcon } from '@heroicons/react/24/outline';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { api } from '../lib/api';
|
import { api } from '../lib/api';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router';
|
||||||
import { EyeIcon, CheckIcon, XMarkIcon, MagnifyingGlassIcon } from '@heroicons/react/24/outline';
|
import { EyeIcon, CheckIcon, XMarkIcon, MagnifyingGlassIcon } from '@heroicons/react/24/outline';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { api } from '../lib/api';
|
import { api } from '../lib/api';
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ vi.mock('../lib/api', () => ({
|
|||||||
ApiError: class extends Error {},
|
ApiError: class extends Error {},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('react-router-dom', async () => {
|
vi.mock('react-router', async () => {
|
||||||
const actual = await vi.importActual<typeof import('react-router-dom')>('react-router-dom');
|
const actual = await vi.importActual<typeof import('react-router')>('react-router');
|
||||||
return { ...actual, useParams: () => ({ uuid: 'ses-1' }) };
|
return { ...actual, useParams: () => ({ uuid: 'ses-1' }) };
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router';
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { api, ApiError, type ApiResponse } from '../lib/api';
|
import { api, ApiError, type ApiResponse } from '../lib/api';
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { ChevronLeftIcon, ClipboardDocumentListIcon, CpuChipIcon } from '@heroicons/react/24/outline';
|
import { ChevronLeftIcon, ClipboardDocumentListIcon, CpuChipIcon } from '@heroicons/react/24/outline';
|
||||||
import { api } from '../lib/api';
|
import { api } from '../lib/api';
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import {
|
|||||||
} from "@heroicons/react/24/outline";
|
} from "@heroicons/react/24/outline";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router";
|
||||||
import TreatmentCaseEditModal from "../components/TreatmentCaseEditModal";
|
import TreatmentCaseEditModal from "../components/TreatmentCaseEditModal";
|
||||||
import {
|
import {
|
||||||
PatientsCategoryView,
|
PatientsCategoryView,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { avatarGradient } from '../lib/avatarColors';
|
import { avatarGradient } from '../lib/avatarColors';
|
||||||
import { useParams, useNavigate, useSearchParams } from 'react-router-dom';
|
import { useParams, useNavigate, useSearchParams } from 'react-router';
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useState, useEffect, useMemo } from 'react';
|
import React, { useState, useEffect, useMemo } from 'react';
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router';
|
||||||
import {
|
import {
|
||||||
MagnifyingGlassIcon, PlusIcon,
|
MagnifyingGlassIcon, PlusIcon,
|
||||||
EyeIcon, TrashIcon, ArrowPathIcon,
|
EyeIcon, TrashIcon, ArrowPathIcon,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { ReactElement, ReactNode } from 'react';
|
import { ReactElement, ReactNode } from 'react';
|
||||||
import { render, renderHook } from '@testing-library/react';
|
import { render, renderHook } from '@testing-library/react';
|
||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
import { MemoryRouter } from 'react-router-dom';
|
import { MemoryRouter } from 'react-router';
|
||||||
|
|
||||||
export function makeClient(): QueryClient {
|
export function makeClient(): QueryClient {
|
||||||
return new QueryClient({
|
return new QueryClient({
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
"symfony/dotenv": "7.4.*",
|
"symfony/dotenv": "7.4.*",
|
||||||
"symfony/flex": "^2",
|
"symfony/flex": "^2",
|
||||||
"symfony/framework-bundle": "7.4.*",
|
"symfony/framework-bundle": "7.4.*",
|
||||||
|
"symfony/html-sanitizer": "7.4.*",
|
||||||
"symfony/http-client": "7.4.*",
|
"symfony/http-client": "7.4.*",
|
||||||
"symfony/messenger": "7.4.*",
|
"symfony/messenger": "7.4.*",
|
||||||
"symfony/property-access": "7.4.*",
|
"symfony/property-access": "7.4.*",
|
||||||
|
|||||||
Generated
+432
-68
@@ -4,7 +4,7 @@
|
|||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "a224d59e9e6ce93b3f3f78ee13b06bf5",
|
"content-hash": "00a0f450cb89b5bc25c645de861adbfb",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "altcha-org/altcha",
|
"name": "altcha-org/altcha",
|
||||||
@@ -1241,6 +1241,188 @@
|
|||||||
],
|
],
|
||||||
"time": "2025-10-17T11:30:53+00:00"
|
"time": "2025-10-17T11:30:53+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "league/uri",
|
||||||
|
"version": "7.8.1",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/thephpleague/uri.git",
|
||||||
|
"reference": "08cf38e3924d4f56238125547b5720496fac8fd4"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/thephpleague/uri/zipball/08cf38e3924d4f56238125547b5720496fac8fd4",
|
||||||
|
"reference": "08cf38e3924d4f56238125547b5720496fac8fd4",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"league/uri-interfaces": "^7.8.1",
|
||||||
|
"php": "^8.1",
|
||||||
|
"psr/http-factory": "^1"
|
||||||
|
},
|
||||||
|
"conflict": {
|
||||||
|
"league/uri-schemes": "^1.0"
|
||||||
|
},
|
||||||
|
"suggest": {
|
||||||
|
"ext-bcmath": "to improve IPV4 host parsing",
|
||||||
|
"ext-dom": "to convert the URI into an HTML anchor tag",
|
||||||
|
"ext-fileinfo": "to create Data URI from file contennts",
|
||||||
|
"ext-gmp": "to improve IPV4 host parsing",
|
||||||
|
"ext-intl": "to handle IDN host with the best performance",
|
||||||
|
"ext-uri": "to use the PHP native URI class",
|
||||||
|
"jeremykendall/php-domain-parser": "to further parse the URI host and resolve its Public Suffix and Top Level Domain",
|
||||||
|
"league/uri-components": "to provide additional tools to manipulate URI objects components",
|
||||||
|
"league/uri-polyfill": "to backport the PHP URI extension for older versions of PHP",
|
||||||
|
"php-64bit": "to improve IPV4 host parsing",
|
||||||
|
"rowbot/url": "to handle URLs using the WHATWG URL Living Standard specification",
|
||||||
|
"symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"extra": {
|
||||||
|
"branch-alias": {
|
||||||
|
"dev-master": "7.x-dev"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"League\\Uri\\": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Ignace Nyamagana Butera",
|
||||||
|
"email": "nyamsprod@gmail.com",
|
||||||
|
"homepage": "https://nyamsprod.com"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "URI manipulation library",
|
||||||
|
"homepage": "https://uri.thephpleague.com",
|
||||||
|
"keywords": [
|
||||||
|
"URN",
|
||||||
|
"data-uri",
|
||||||
|
"file-uri",
|
||||||
|
"ftp",
|
||||||
|
"hostname",
|
||||||
|
"http",
|
||||||
|
"https",
|
||||||
|
"middleware",
|
||||||
|
"parse_str",
|
||||||
|
"parse_url",
|
||||||
|
"psr-7",
|
||||||
|
"query-string",
|
||||||
|
"querystring",
|
||||||
|
"rfc2141",
|
||||||
|
"rfc3986",
|
||||||
|
"rfc3987",
|
||||||
|
"rfc6570",
|
||||||
|
"rfc8141",
|
||||||
|
"uri",
|
||||||
|
"uri-template",
|
||||||
|
"url",
|
||||||
|
"ws"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"docs": "https://uri.thephpleague.com",
|
||||||
|
"forum": "https://thephpleague.slack.com",
|
||||||
|
"issues": "https://github.com/thephpleague/uri-src/issues",
|
||||||
|
"source": "https://github.com/thephpleague/uri/tree/7.8.1"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://github.com/sponsors/nyamsprod",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2026-03-15T20:22:25+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "league/uri-interfaces",
|
||||||
|
"version": "7.8.1",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/thephpleague/uri-interfaces.git",
|
||||||
|
"reference": "85d5c77c5d6d3af6c54db4a78246364908f3c928"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/85d5c77c5d6d3af6c54db4a78246364908f3c928",
|
||||||
|
"reference": "85d5c77c5d6d3af6c54db4a78246364908f3c928",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"ext-filter": "*",
|
||||||
|
"php": "^8.1",
|
||||||
|
"psr/http-message": "^1.1 || ^2.0"
|
||||||
|
},
|
||||||
|
"suggest": {
|
||||||
|
"ext-bcmath": "to improve IPV4 host parsing",
|
||||||
|
"ext-gmp": "to improve IPV4 host parsing",
|
||||||
|
"ext-intl": "to handle IDN host with the best performance",
|
||||||
|
"php-64bit": "to improve IPV4 host parsing",
|
||||||
|
"rowbot/url": "to handle URLs using the WHATWG URL Living Standard specification",
|
||||||
|
"symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"extra": {
|
||||||
|
"branch-alias": {
|
||||||
|
"dev-master": "7.x-dev"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"League\\Uri\\": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Ignace Nyamagana Butera",
|
||||||
|
"email": "nyamsprod@gmail.com",
|
||||||
|
"homepage": "https://nyamsprod.com"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Common tools for parsing and resolving RFC3987/RFC3986 URI",
|
||||||
|
"homepage": "https://uri.thephpleague.com",
|
||||||
|
"keywords": [
|
||||||
|
"data-uri",
|
||||||
|
"file-uri",
|
||||||
|
"ftp",
|
||||||
|
"hostname",
|
||||||
|
"http",
|
||||||
|
"https",
|
||||||
|
"parse_str",
|
||||||
|
"parse_url",
|
||||||
|
"psr-7",
|
||||||
|
"query-string",
|
||||||
|
"querystring",
|
||||||
|
"rfc3986",
|
||||||
|
"rfc3987",
|
||||||
|
"rfc6570",
|
||||||
|
"uri",
|
||||||
|
"url",
|
||||||
|
"ws"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"docs": "https://uri.thephpleague.com",
|
||||||
|
"forum": "https://thephpleague.slack.com",
|
||||||
|
"issues": "https://github.com/thephpleague/uri-src/issues",
|
||||||
|
"source": "https://github.com/thephpleague/uri-interfaces/tree/7.8.1"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://github.com/sponsors/nyamsprod",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2026-03-08T20:05:35+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "lexik/jwt-authentication-bundle",
|
"name": "lexik/jwt-authentication-bundle",
|
||||||
"version": "v3.2.0",
|
"version": "v3.2.0",
|
||||||
@@ -1357,6 +1539,73 @@
|
|||||||
],
|
],
|
||||||
"time": "2025-12-20T17:47:00+00:00"
|
"time": "2025-12-20T17:47:00+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "masterminds/html5",
|
||||||
|
"version": "2.10.1",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/Masterminds/html5-php.git",
|
||||||
|
"reference": "fd5018f6815fff903946d0564977b44ce8010e29"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/Masterminds/html5-php/zipball/fd5018f6815fff903946d0564977b44ce8010e29",
|
||||||
|
"reference": "fd5018f6815fff903946d0564977b44ce8010e29",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"ext-dom": "*",
|
||||||
|
"php": ">=5.3.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"phpunit/phpunit": "^4.8.35 || ^5.7.21 || ^6 || ^7 || ^8 || ^9 || ^10"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"extra": {
|
||||||
|
"branch-alias": {
|
||||||
|
"dev-master": "2.7-dev"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Masterminds\\": "src"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Matt Butcher",
|
||||||
|
"email": "technosophos@gmail.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Matt Farina",
|
||||||
|
"email": "matt@mattfarina.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Asmir Mustafic",
|
||||||
|
"email": "goetas@gmail.com"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "An HTML5 parser and serializer.",
|
||||||
|
"homepage": "http://masterminds.github.io/html5-php",
|
||||||
|
"keywords": [
|
||||||
|
"HTML5",
|
||||||
|
"dom",
|
||||||
|
"html",
|
||||||
|
"parser",
|
||||||
|
"querypath",
|
||||||
|
"serializer",
|
||||||
|
"xml"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/Masterminds/html5-php/issues",
|
||||||
|
"source": "https://github.com/Masterminds/html5-php/tree/2.10.1"
|
||||||
|
},
|
||||||
|
"time": "2026-06-23T18:43:15+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "nelmio/api-doc-bundle",
|
"name": "nelmio/api-doc-bundle",
|
||||||
"version": "v5.10.3",
|
"version": "v5.10.3",
|
||||||
@@ -2025,6 +2274,114 @@
|
|||||||
},
|
},
|
||||||
"time": "2019-01-08T18:20:26+00:00"
|
"time": "2019-01-08T18:20:26+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "psr/http-factory",
|
||||||
|
"version": "1.1.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/php-fig/http-factory.git",
|
||||||
|
"reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a",
|
||||||
|
"reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": ">=7.1",
|
||||||
|
"psr/http-message": "^1.0 || ^2.0"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"extra": {
|
||||||
|
"branch-alias": {
|
||||||
|
"dev-master": "1.0.x-dev"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Psr\\Http\\Message\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "PHP-FIG",
|
||||||
|
"homepage": "https://www.php-fig.org/"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "PSR-17: Common interfaces for PSR-7 HTTP message factories",
|
||||||
|
"keywords": [
|
||||||
|
"factory",
|
||||||
|
"http",
|
||||||
|
"message",
|
||||||
|
"psr",
|
||||||
|
"psr-17",
|
||||||
|
"psr-7",
|
||||||
|
"request",
|
||||||
|
"response"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"source": "https://github.com/php-fig/http-factory"
|
||||||
|
},
|
||||||
|
"time": "2024-04-15T12:06:14+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "psr/http-message",
|
||||||
|
"version": "2.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/php-fig/http-message.git",
|
||||||
|
"reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71",
|
||||||
|
"reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": "^7.2 || ^8.0"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"extra": {
|
||||||
|
"branch-alias": {
|
||||||
|
"dev-master": "2.0.x-dev"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Psr\\Http\\Message\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "PHP-FIG",
|
||||||
|
"homepage": "https://www.php-fig.org/"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Common interface for HTTP messages",
|
||||||
|
"homepage": "https://github.com/php-fig/http-message",
|
||||||
|
"keywords": [
|
||||||
|
"http",
|
||||||
|
"http-message",
|
||||||
|
"psr",
|
||||||
|
"psr-7",
|
||||||
|
"request",
|
||||||
|
"response"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"source": "https://github.com/php-fig/http-message/tree/2.0"
|
||||||
|
},
|
||||||
|
"time": "2023-04-04T09:54:51+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "psr/log",
|
"name": "psr/log",
|
||||||
"version": "3.0.2",
|
"version": "3.0.2",
|
||||||
@@ -3687,6 +4044,80 @@
|
|||||||
],
|
],
|
||||||
"time": "2026-06-27T08:31:38+00:00"
|
"time": "2026-06-27T08:31:38+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "symfony/html-sanitizer",
|
||||||
|
"version": "v7.4.14",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/symfony/html-sanitizer.git",
|
||||||
|
"reference": "c328df69f5b6f44a0d031d757903d955bebb23b3"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/symfony/html-sanitizer/zipball/c328df69f5b6f44a0d031d757903d955bebb23b3",
|
||||||
|
"reference": "c328df69f5b6f44a0d031d757903d955bebb23b3",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"ext-dom": "*",
|
||||||
|
"league/uri": "^6.5|^7.0",
|
||||||
|
"masterminds/html5": "^2.7.2",
|
||||||
|
"php": ">=8.2",
|
||||||
|
"symfony/deprecation-contracts": "^2.5|^3"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Symfony\\Component\\HtmlSanitizer\\": ""
|
||||||
|
},
|
||||||
|
"exclude-from-classmap": [
|
||||||
|
"/Tests/"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Titouan Galopin",
|
||||||
|
"email": "galopintitouan@gmail.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Symfony Community",
|
||||||
|
"homepage": "https://symfony.com/contributors"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Provides an object-oriented API to sanitize untrusted HTML input for safe insertion into a document's DOM.",
|
||||||
|
"homepage": "https://symfony.com",
|
||||||
|
"keywords": [
|
||||||
|
"Purifier",
|
||||||
|
"html",
|
||||||
|
"sanitizer"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"source": "https://github.com/symfony/html-sanitizer/tree/v7.4.14"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://symfony.com/sponsor",
|
||||||
|
"type": "custom"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/fabpot",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/nicolas-grekas",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||||
|
"type": "tidelift"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2026-06-06T11:10:32+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/http-client",
|
"name": "symfony/http-client",
|
||||||
"version": "v7.4.14",
|
"version": "v7.4.14",
|
||||||
@@ -7442,73 +7873,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"packages-dev": [
|
"packages-dev": [
|
||||||
{
|
|
||||||
"name": "masterminds/html5",
|
|
||||||
"version": "2.10.1",
|
|
||||||
"source": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://github.com/Masterminds/html5-php.git",
|
|
||||||
"reference": "fd5018f6815fff903946d0564977b44ce8010e29"
|
|
||||||
},
|
|
||||||
"dist": {
|
|
||||||
"type": "zip",
|
|
||||||
"url": "https://api.github.com/repos/Masterminds/html5-php/zipball/fd5018f6815fff903946d0564977b44ce8010e29",
|
|
||||||
"reference": "fd5018f6815fff903946d0564977b44ce8010e29",
|
|
||||||
"shasum": ""
|
|
||||||
},
|
|
||||||
"require": {
|
|
||||||
"ext-dom": "*",
|
|
||||||
"php": ">=5.3.0"
|
|
||||||
},
|
|
||||||
"require-dev": {
|
|
||||||
"phpunit/phpunit": "^4.8.35 || ^5.7.21 || ^6 || ^7 || ^8 || ^9 || ^10"
|
|
||||||
},
|
|
||||||
"type": "library",
|
|
||||||
"extra": {
|
|
||||||
"branch-alias": {
|
|
||||||
"dev-master": "2.7-dev"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"autoload": {
|
|
||||||
"psr-4": {
|
|
||||||
"Masterminds\\": "src"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"notification-url": "https://packagist.org/downloads/",
|
|
||||||
"license": [
|
|
||||||
"MIT"
|
|
||||||
],
|
|
||||||
"authors": [
|
|
||||||
{
|
|
||||||
"name": "Matt Butcher",
|
|
||||||
"email": "technosophos@gmail.com"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Matt Farina",
|
|
||||||
"email": "matt@mattfarina.com"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Asmir Mustafic",
|
|
||||||
"email": "goetas@gmail.com"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"description": "An HTML5 parser and serializer.",
|
|
||||||
"homepage": "http://masterminds.github.io/html5-php",
|
|
||||||
"keywords": [
|
|
||||||
"HTML5",
|
|
||||||
"dom",
|
|
||||||
"html",
|
|
||||||
"parser",
|
|
||||||
"querypath",
|
|
||||||
"serializer",
|
|
||||||
"xml"
|
|
||||||
],
|
|
||||||
"support": {
|
|
||||||
"issues": "https://github.com/Masterminds/html5-php/issues",
|
|
||||||
"source": "https://github.com/Masterminds/html5-php/tree/2.10.1"
|
|
||||||
},
|
|
||||||
"time": "2026-06-23T18:43:15+00:00"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "myclabs/deep-copy",
|
"name": "myclabs/deep-copy",
|
||||||
"version": "1.13.4",
|
"version": "1.13.4",
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
# Sanitizer for rich text that a human authored in the admin panel and that the
|
||||||
|
# panel later renders with `dangerouslySetInnerHTML` (BlogReviewPage).
|
||||||
|
#
|
||||||
|
# آدیت ۲۰۲۶-۰۸-۰۷: بدنهٔ مقاله همانطور که میآمد ذخیره و رندر میشد. CSP فعلی
|
||||||
|
# (`script-src 'self'`) اسکریپت تزریقی را اجرا نمیکند، ولی هندلرهای inline و
|
||||||
|
# `javascript:` را کامل نمیبندد — و CSP لایهٔ دوم است، نه اولین دفاع. پاکسازی
|
||||||
|
# هنگام **ذخیره** انجام میشود نه هنگام نمایش، چون یک نقطهٔ ورود دارد و هر
|
||||||
|
# مصرفکنندهٔ بعدی (پنل، سایت عمومی، فید) خودبهخود امن میشود.
|
||||||
|
#
|
||||||
|
# فهرست عناصر = چیزی که CKEditor 5 واقعاً تولید میکند. عنصری که اینجا نیست حذف
|
||||||
|
# میشود ولی متنش میماند، پس محتوای قدیمی نامفهوم نمیشود.
|
||||||
|
framework:
|
||||||
|
html_sanitizer:
|
||||||
|
sanitizers:
|
||||||
|
blog.body:
|
||||||
|
allow_safe_elements: true
|
||||||
|
allow_elements:
|
||||||
|
p: []
|
||||||
|
br: []
|
||||||
|
hr: []
|
||||||
|
strong: []
|
||||||
|
b: []
|
||||||
|
em: []
|
||||||
|
i: []
|
||||||
|
u: []
|
||||||
|
s: []
|
||||||
|
sub: []
|
||||||
|
sup: []
|
||||||
|
mark: []
|
||||||
|
h1: []
|
||||||
|
h2: []
|
||||||
|
h3: []
|
||||||
|
h4: []
|
||||||
|
h5: []
|
||||||
|
h6: []
|
||||||
|
ul: []
|
||||||
|
ol: ['start', 'reversed']
|
||||||
|
li: []
|
||||||
|
blockquote: []
|
||||||
|
pre: []
|
||||||
|
code: []
|
||||||
|
figure: ['class']
|
||||||
|
figcaption: []
|
||||||
|
span: []
|
||||||
|
div: []
|
||||||
|
a: ['href', 'title', 'target', 'rel']
|
||||||
|
img: ['src', 'alt', 'title', 'width', 'height']
|
||||||
|
table: []
|
||||||
|
thead: []
|
||||||
|
tbody: []
|
||||||
|
tfoot: []
|
||||||
|
tr: []
|
||||||
|
td: ['colspan', 'rowspan']
|
||||||
|
th: ['colspan', 'rowspan', 'scope']
|
||||||
|
# فقط طرحهای امن. `javascript:` عمداً نیست.
|
||||||
|
allowed_link_schemes: ['http', 'https', 'mailto']
|
||||||
|
allowed_media_schemes: ['http', 'https', 'data']
|
||||||
|
allow_relative_links: true
|
||||||
|
# لینک بیرونی در تب جدید باز میشود؛ بدون اینها tabnabbing باز است.
|
||||||
|
force_attributes:
|
||||||
|
a:
|
||||||
|
rel: 'noopener noreferrer'
|
||||||
|
# `drop` نه `block`: block فقط تگ را برمیدارد و متنِ داخلش را نگه
|
||||||
|
# میدارد، یعنی `<script>alert(1)</script>` به متنِ `alert(1)` تبدیل
|
||||||
|
# میشد. برای این عناصر، خودِ محتوا هم باید برود.
|
||||||
|
drop_elements: ['script', 'style', 'iframe', 'object', 'embed', 'form', 'input', 'button', 'noscript']
|
||||||
|
max_input_length: 500000
|
||||||
@@ -653,7 +653,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
|||||||
* time_based_uuid_node?: scalar|Param|null,
|
* time_based_uuid_node?: scalar|Param|null,
|
||||||
* },
|
* },
|
||||||
* html_sanitizer?: bool|array{ // HtmlSanitizer configuration
|
* html_sanitizer?: bool|array{ // HtmlSanitizer configuration
|
||||||
* enabled?: bool|Param, // Default: false
|
* enabled?: bool|Param, // Default: true
|
||||||
* sanitizers?: array<string, array{ // Default: []
|
* sanitizers?: array<string, array{ // Default: []
|
||||||
* allow_safe_elements?: bool|Param, // Allows "safe" elements and attributes. // Default: false
|
* allow_safe_elements?: bool|Param, // Allows "safe" elements and attributes. // Default: false
|
||||||
* allow_static_elements?: bool|Param, // Allows all static elements and attributes from the W3C Sanitizer API standard. // Default: false
|
* allow_static_elements?: bool|Param, // Allows all static elements and attributes from the W3C Sanitizer API standard. // Default: false
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user