feat: implement OTP login flow and enhance role-based access control

- Added OTP login functionality in driver.mjs to handle user authentication with a fixed code in dev environment.
- Enhanced RoleRoute component in App.tsx to support clinic-scoped doctor roles and permissions.
- Updated ClinicDoctorsManager component to include pagination and search functionality for better user experience.
- Refactored tests for ClinicDoctorsManager to cover new features and ensure proper API mocking.
- Adjusted permissions in settingsMenu.ts and PracticeDomainSettingsPage.tsx to align with updated backend requirements.
- Created RoleRoute.test.tsx to validate role-based access logic for different user scenarios.
This commit is contained in:
hamed
2026-08-09 12:41:11 +03:30
parent cfeb447645
commit 60ccd5cc1d
8 changed files with 515 additions and 121 deletions
+55 -14
View File
@@ -26,7 +26,7 @@
* `ds` prints the design system — tokens from styles.css and the shared
* components — so a redesign starts from what exists.
*/
import { spawn } from 'node:child_process';
import { spawn, execSync } from 'node:child_process';
import { readFileSync, writeFileSync, existsSync, readdirSync, mkdirSync } from 'node:fs';
import { resolve, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
@@ -82,6 +82,33 @@ function cdp(ws) {
});
}
/**
* ورود با OTP — در dev کد ثابتِ `12345` است (OtpService::sendCode)، پس کل زنجیرهٔ
* send-code → verify-code → otp-login اسکریپت‌پذیر است.
*
* راهِ نجاتِ دیتابیسی که seed نشده: کاربر واقعیِ چنین دیتابیسی ممکن است اصلاً
* `password_hash` نداشته باشد یا رمزش را ندانیم، ولی شمارهٔ موبایلش کافی است.
*/
async function otpLogin(mobile) {
const post = async (path, body) => {
const r = await fetch(`${BASE}${path}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
return r.json();
};
const sent = await post('/api/v1/user/send-code', { mobile });
if (!sent.uuid) throw new Error(`send-code failed: ${JSON.stringify(sent).slice(0, 160)}`);
const ver = await post('/api/v1/user/verify-code', { uuid: sent.uuid, code: '12345' });
const grant = ver?.data?.grant;
if (!grant) throw new Error(`verify-code failed: ${JSON.stringify(ver).slice(0, 160)}`);
return post('/api/v1/user/otp-login', { grant });
}
async function login() {
const r = await fetch(`${BASE}/api/v1/user/login`, {
method: 'POST',
@@ -89,14 +116,30 @@ async function login() {
body: JSON.stringify({ mobile_number: USER, password: PASS }),
});
const j = await r.json();
if (!j.access_token) {
throw new Error(
`login failed for ${USER}: ${JSON.stringify(j).slice(0, 160)}\n`
+ ' → seeded users are in TEST_USERS.md; rebuild them with\n'
+ ' ddev exec php bin/console app:seed-scenarios --reset -n',
if (j.access_token) return j;
// رمز نخورد؛ با OTP امتحان کن. دیتابیس‌های واقعی (نه seed) رمزِ مستندشده ندارند و
// بدون این، تنها راه یا reset کردن دیتابیس کاربر بود یا نوشتنِ رمز روی حسابش.
const otp = await otpLogin(USER).catch((e) => ({ _err: e.message }));
if (otp.access_token) return otp;
// `send-code` سقف ۵ بار در ساعت دارد. وقتی سوخت، به‌جای شل‌کردن یک محدودیتِ واقعیِ
// محصول برای تست، توکن را با کامندِ خودِ اپ می‌سازیم — همان کلید و همان claimها.
try {
const out = execSync(
`ddev exec 'php bin/console lexik:jwt:generate-token ${USER} --user-class="App\\\\Auth\\\\Entity\\\\User"'`,
{ encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] },
);
}
return j;
const minted = out.trim().split('\n').pop().trim();
if (minted.startsWith('ey')) return { access_token: minted, refresh_token: null };
} catch { /* کامند در دسترس نیست؛ می‌افتیم روی خطای زیر */ }
throw new Error(
`login failed for ${USER}: ${JSON.stringify(j).slice(0, 160)}\n`
+ ` → OTP fallback also failed: ${otp._err ?? JSON.stringify(otp).slice(0, 120)}\n`
+ ' → CLINICPRO_USER را روی شمارهٔ یک کاربر واقعیِ همین دیتابیس بگذار،\n'
+ ' یا حساب‌های تست را بساز: ddev exec php bin/console app:seed-scenarios --reset -n',
);
}
/**
@@ -104,12 +147,10 @@ async function login() {
* می‌نشیند و صفحه‌های کلینیک خالی می‌آیند — که شبیه باگ است ولی نیست.
*/
async function switchContext(token, kind) {
const r = await fetch(`${BASE}/api/v1/auth/contexts`, {
headers: { Authorization: `Bearer ${token}` },
}).catch(() => null);
// اندپوینت فهرست محیط‌ها عمومی نیست؛ uuid را از /api/v1/user/me می‌گیریم.
const me = await (await fetch(`${BASE}/api/v1/user/me`, {
// فهرست محیط‌ها فقط در `/oauth/userinfo` است. مسیر قبلی (`/api/v1/user/me`) اصلاً
// وجود ندارد و ۴۰۴ می‌داد، پس `--context clinic` همیشه بی‌صدا نادیده گرفته می‌شد و
// اسکرین‌شاتِ محیطِ اشتباه گرفته می‌شد.
const me = await (await fetch(`${BASE}/oauth/userinfo`, {
headers: { Authorization: `Bearer ${token}` },
})).json().catch(() => ({}));