Files
clinicpro/.claude/prompt/user-profile-resolve-by-user-uuid.md
T
hamedandClaude Opus 4.8 da57ac9c5b fix(user-profile): resolve profile by user uuid and auto-create when missing
GET/PATCH /api/v1/user-profile/{uuid} treated {uuid} as the profile's own
uuid, but clients pass the user's uuid — and a freshly OTP-registered user
has no profile row, so the call always 404'd. Add resolveProfile(): try
profile uuid, then user uuid → that user's profile, and (for the current
user or an admin) lazy-create an empty profile so the client always gets
an editable one. Foreign/unknown uuids still 404 with no leak.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 19:01:27 +03:30

9.4 KiB
Raw Blame History

رفع 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

#[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 مستقل از کاربر

#[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ها

// UserProfileRepository
public function findByUser(User $user): ?UserProfile { ... }
public function findByUuid(string $uuid): ?UserProfile { ... }
// UserRepository
public function findByUuid(string $uuid): ?User { ... }

وظایف

۱. تزریق UserRepository به کنترلر

public function __construct(
    private readonly UserProfileRepository $repository,
    private readonly UserRepository        $userRepository,
) {}

۲. بازنویسی show() — resolve با profile-uuid یا user-uuid + پروفایل خالی برای کاربر جدید

#[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 را به‌روز کن.