7 Commits
Author SHA1 Message Date
hamedandClaude Opus 5 74d2034158 fix(auth): offer every clinic a user owns as a switchable context
buildAvailableContexts used ClinicRepository::findByUser(), which is a
findOneBy — so a user who owns two clinics only ever saw the first one.
switchContext validates its input against that same list, so the second
clinic could not be selected at all.

Before tenant isolation this was merely annoying. Since phase 4 it is a
blocker: an environment that cannot be selected is an environment
TenantFilter hides from its own owner. Found by running the suite against
an imported production database, where one account owns two clinics and
its second clinic had become unreachable.

findByUser() stays for the fallbacks that only need "some clinic"; the
context list now uses findAllByUser(). The other 20 findByUser() call sites
are single-clinic fallbacks used when no context is chosen, and keep their
current behaviour — once the owner can switch, UserActiveContext decides.

Removing the fix turns 3 of the 4 new tests red.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 16:11:49 +03:30
hamed 74577c2ff6 feat: unify doctor title handling and enhance specialty selection
- Implemented a helper function `displayDoctorName` to prepend "دکتر" to doctor names for consistent display across the application.
- Updated various components (InviteDoctorModal, DashboardPage, DoctorDetailPage, DoctorsPage, etc.) to utilize the new helper for rendering doctor names.
- Modified the DoctorFormPage to automatically add the "دکتر" title in the UI without requiring user input.
- Fixed the EditSpecialtyPicker component to allow multiple specialty selections, resolving a UI bug where only one specialty could be selected at a time.
- Ensured that the backend strips the "دکتر" title from the name during pre-registration and doctor creation processes.
- Added tests for the new functionality, including checks for title handling and specialty selection logic.
- Updated API documentation to reflect changes in name handling and display logic.
2026-07-19 19:57:03 +03:30
hamedandClaude Opus 4.8 76f9fbe88f feat(settings): complete remaining settings tabs (account, tags, turns)
Fill the three previously-placeholder settings sections so every menu item
is now a real page inside the settings shell:

- حساب کاربری: new authenticated POST /api/v1/user/change-password
  (verifies current password, ≥8 chars, must differ) + account page with a
  profile summary and change-password form.
- برچسب‌ها: new per-tenant TenantTag domain (entity/repo/controller +
  migration) with tenant-scoped CRUD at /api/v1/tenant-tag(s), plus a tags
  management page (list + color + add/edit/delete).
- مدیریت نوبت دهی: export the existing WeeklyScheduleTab from
  DoctorDetailPage and reuse it in a standalone AppointmentSettingsPage
  (current doctor's uuid + addresses).

Wire all three menu entries to their routes. Backend covered by PHPUnit
(change-password, tenant-tag CRUD + ownership); FE covered by Vitest.
API docs updated (auth.md, tag.md).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 14:16:21 +03:30
hamed 8705b88270 fix(auth): update refresh token behavior to be reusable within TTL and add tests for token functionality 2026-06-28 21:57:42 +03:30
hamed c2b47d6396 delete(tests): remove RefreshTokenRotationTest as it is no longer needed 2026-06-28 21:52:52 +03:30
hamedandClaude Opus 4.8 61ac775175 perf(settlement,rating): paginate 3 unbounded list endpoints (M10-M12)
- M10 GET /settlement: was unbounded; add page/limit + countByUser + data.meta.
- M11 GET /admin/comments/pending: paginate findPending + countPending.
- M12 GET /comments/{doctor}: paginate the fetch-joined roots query via
  Paginator(fetchJoinCollection) + countApprovedRootsByDoctor.

All keep the existing { data: { data: [...] } } envelope and add data.meta
(backward compatible). Default limit 50 / max 100.

Regressions: SettlementListPaginationTest, CommentPaginationTest (both fail
without the limits). Also de-flaked SendCodeMobileRateLimitTest (randomised the
IP block so the persistent per-IP limiter buckets don't accumulate across runs).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 20:36:41 +03:30
hamedandClaude Opus 4.8 670cef24f4 fix(security): per-mobile OTP cap + refresh-token rotation (M6, M7)
M6: send-code rate-limited only per IP, so a victim's number could be
SMS-flooded from rotating IPs. Add a per-mobile bucket (same 5/hour policy)
keyed by the validated mobile.

M7: /oauth/token/refresh reused the presented refresh token verbatim (no
rotation) and never re-checked the user. The rotation infra already existed
(issueTokens mints a fresh refresh token) — the controller just discarded it.
Now revoke the presented token (single-use), issue a fresh pair, and reject a
suspended user (status != 1).

Regressions: tests/Auth/SendCodeMobileRateLimitTest,
tests/Auth/RefreshTokenRotationTest (both fail without the fix).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 20:25:22 +03:30