# رفع 404 پروفایل کاربر: resolve با user-uuid و بازگرداندن پروفایل خالی برای کاربر تازه ## پروژه `clinicpro` (Backend). **این پرامپت اول اجرا شود.** > **Cross-repo:** قرارداد `GET /api/v1/user-profile/{uuid}` توسط سایت عمومی (`nobat724_front`) مصرف می‌شود. پرامپت همتای frontend: > `nobat724_front/.claude/prompt/user-profile-no-404.md` ## زمینه سایت عمومی بعد از لاگین، پروفایل کاربر را با `GET /api/v1/user-profile/{uuid}` می‌گیرد و `uuid` را از کوکی auth (یعنی **uuid کاربر**) می‌فرستد. اما این endpoint پروفایل را با `findByUuid($uuid)` پیدا می‌کند که `{uuid}` را **uuid خودِ پروفایل** فرض می‌کند — نه uuid کاربر. `UserProfile` یک `uuid` مستقل دارد (هنگام ساخت پروفایل تولید می‌شود) که با uuid کاربر فرق دارد. علاوه بر این، کاربری که تازه با OTP ثبت‌نام کرده **اصلاً ردیف پروفایل ندارد** (فلوی OTP فقط `User` می‌سازد؛ پروفایل با `POST /user-profile` ساخته می‌شود). نتیجه: برای کاربر `09210651788` (تأییدشده در DB: `user_uuid=4d19830c-842a-4a79-b56c-198110cd73e2`, `profile_uuid=NULL`)، فراخوانی `GET /user-profile/4d19830c-...` همیشه **404 «پروفایل یافت نشد»** می‌دهد — هم به‌خاطر mismatch و هم به‌خاطر نبودِ پروفایل. این داشبورد و مرحله‌ی تکمیل اطلاعاتِ نوبت‌گیری را برای هر کاربر جدید می‌شکند. ## مشکل / هدف `GET /api/v1/user-profile/{uuid}` باید: 1. `{uuid}` را **هم به‌عنوان uuid پروفایل و هم uuid کاربر** قبول کند (مثل الگوی weekly-schedule که هر دو را امتحان می‌کند). 2. اگر کاربرِ احرازشده پروفایل ندارد، به‌جای 404 یک **پروفایل خالی برای همان کاربر بسازد و برگرداند** (یا یک شیء پروفایل پیش‌فرض با مقادیر null) — تا frontend همیشه یک پروفایل قابل‌نمایش/ویرایش بگیرد. 3. کنترل دسترسی حفظ شود: کاربر فقط پروفایل خودش (یا ادمین هر پروفایلی). ## فایل‌های مرتبط | فایل | نقش | |------|-----| | `src/UserProfile/Controller/UserProfileController.php` | `show()` (GET) — محل اصلی رفع؛ `canAccess`, `hydrate` | | `src/UserProfile/Repository/UserProfileRepository.php` | `findByUuid`, `findByUser` | | `src/Auth/Repository/UserRepository.php` | `findByUuid` (برای resolve با user-uuid) | | `src/UserProfile/Entity/UserProfile.php` | `uuid` مستقل + `getUser()`؛ `toArray()` شامل `uuid` و `user_uuid` | | `docs/api/user-profile.md` | مستندسازی رفتار جدید GET | ## وضعیت فعلی (کد واقعی) ### `UserProfileController::show()` — فقط با profile-uuid ```php #[Route('/api/v1/user-profile/{uuid}', methods: ['GET'])] public function show(string $uuid, #[CurrentUser] User $user): JsonResponse { $profile = $this->repository->findByUuid($uuid); // ❌ فقط profile uuid if ($profile === null) { return $this->error(ErrorCodes::ERR_VALIDATION_002, 'پروفایل یافت نشد', 404); // ❌ کاربر جدید همیشه اینجا } if (!$this->canAccess($profile, $user)) { return $this->error(ErrorCodes::ERR_AUTH_006, 'دسترسی ممنوع', 403); } return $this->success(['data' => $profile->toArray()]); } ``` ### `UserProfile` — uuid مستقل از کاربر ```php #[ORM\Column(type: 'string', length: 36, unique: true)] private string $uuid; // پروفایل، تولیدشده هنگام ساخت #[ORM\OneToOne(targetEntity: User::class)] private User $user; // کاربر مالک // toArray(): 'uuid' => profile uuid, 'user_uuid' => $this->user->getUuid() ``` ### repositoryها ```php // UserProfileRepository public function findByUser(User $user): ?UserProfile { ... } public function findByUuid(string $uuid): ?UserProfile { ... } // UserRepository public function findByUuid(string $uuid): ?User { ... } ``` ## وظایف ### ۱. تزریق `UserRepository` به کنترلر ```php public function __construct( private readonly UserProfileRepository $repository, private readonly UserRepository $userRepository, ) {} ``` ### ۲. بازنویسی `show()` — resolve با profile-uuid یا user-uuid + پروفایل خالی برای کاربر جدید ```php #[Route('/api/v1/user-profile/{uuid}', methods: ['GET'])] public function show(string $uuid, #[CurrentUser] User $user): JsonResponse { // 1) تلاش با profile uuid $profile = $this->repository->findByUuid($uuid); // 2) fallback: uuid را به‌عنوان user uuid تفسیر کن if ($profile === null) { $targetUser = $this->userRepository->findByUuid($uuid); if ($targetUser !== null) { $profile = $this->repository->findByUser($targetUser); // 3) کاربر وجود دارد ولی پروفایل ندارد → برای خودِ کاربر، پروفایل خالی بساز/برگردان if ($profile === null) { if ($targetUser->getId() !== $user->getId() && !$user->hasRole('ROLE_ADMIN')) { return $this->error(ErrorCodes::ERR_AUTH_006, 'دسترسی ممنوع', 403); } $profile = new UserProfile($targetUser); $this->repository->save($profile); // lazy-create تا frontend پروفایل قابل‌ویرایش بگیرد } } } if ($profile === null) { return $this->error(ErrorCodes::ERR_VALIDATION_002, 'پروفایل یافت نشد', 404); } if (!$this->canAccess($profile, $user)) { return $this->error(ErrorCodes::ERR_AUTH_006, 'دسترسی ممنوع', 403); } return $this->success(['data' => $profile->toArray()]); } ``` > **تصمیم lazy-create:** ساخت پروفایل خالی هنگام اولین GET، ساده‌ترین راه است تا frontend همیشه یک شیء با `uuid`/`user_uuid` بگیرد و PATCH بعدی کار کند. اگر ترجیح می‌دهی **بدون ساخت در DB** فقط یک پروفایل پیش‌فرضِ in-memory برگردانی (بدون persist)، آن هم قابل‌قبول است — ولی آنگاه PATCH با profile-uuid کار نمی‌کند تا اول POST شود. **یکی را انتخاب کن و در گزارش ذکر کن؛ lazy-create توصیه می‌شود.** ### ۳. (اختیاری) PATCH هم با user-uuid کار کند - اگر می‌خواهی فلوی frontend بدون نیاز به دانستن profile-uuid کامل شود، در `update()` (PATCH) هم همان resolve دو-مرحله‌ای را اعمال کن (profile uuid → user uuid → پروفایلِ کاربر). اگر پروفایل نبود، یا بساز یا 404. این کار `POST` و `PATCH` جداگانه در frontend را ساده می‌کند. - اگر این را انجام دادی، در مستندات ذکر کن. ### ۴. مستندسازی در `docs/api/user-profile.md` بخش GET: توضیح بده `{uuid}` می‌تواند **profile uuid یا user uuid** باشد، و برای کاربری که هنوز پروفایل ندارد یک پروفایل خالی (با فیلدهای null) برگردانده می‌شود (و در صورت lazy-create، ساخته می‌شود). همان برای PATCH اگر تغییر دادی. ## نکات مهم - **هیچ تغییر Entity یا migration لازم نیست** (فقط منطق کنترلر). - **دسترسی:** کاربر فقط پروفایل خودش؛ ادمین همه. هنگام resolve با user-uuid، قبل از lazy-create حتماً چک کن `targetUser` همان کاربر احرازشده است (یا ادمین) — وگرنه 403. سپس `canAccess` نهایی هم اعمال شود. - **idempotency:** `POST /user-profile` از قبل اگر پروفایل وجود داشته باشد 409 می‌دهد؛ بعد از lazy-create در GET، فراخوانی POST بعدیِ frontend ممکن است 409 بگیرد — frontend باید با این سازگار شود (در پرامپت frontend ذکر شده). به همین دلیل بهتر است frontend به‌جای POST از PATCH استفاده کند. - پاسخ‌ها از `BaseController` (`success`/`error`)؛ خطاها با `ErrorCodes`. - `toArray()` هم `uuid` (پروفایل) و هم `user_uuid` را برمی‌گرداند — frontend می‌تواند بعد از اولین GET، `uuid` پروفایل را برای PATCHهای بعدی نگه دارد. - تست: - `ddev exec php -l src/UserProfile/Controller/UserProfileController.php` - با توکن کاربر `09210651788` (که `profile_uuid=NULL` است): `GET /api/v1/user-profile/4d19830c-842a-4a79-b56c-198110cd73e2` باید **200** با پروفایل خالی برگرداند (نه 404). - GET با profile-uuid واقعی هم باید کار کند (عدم رگرسیون). - GET با user-uuidِ کاربر دیگر (نه ادمین) → 403. - بعد از تغییر، `docs/api/user-profile.md` را به‌روز کن.