# راهنمای گرفتن توکن و ورود کلاینت به کلاس‌های plugNmeet این راهنما خلاصه می‌کند که برای سناریوی استاد/دانشجو چگونه از سرویس plugNmeet توکن بگیریم و کلاینت فرانت‌اند (`client/`) با آن وارد کلاس شود. ## پیش‌نیازها - آدرس سرویس: `window.PLUG_N_MEET_SERVER_URL = "https://meet.newhorizonco.uk"` (در `config.js`). - `api_key` و `secret` از فایل پیکربندی بک‌اند (`services/plugnmeet-server/config.yaml`). - بدنهٔ درخواست‌ها باید با پروتکل JSON متناظر با پیام‌های پروتوباف (`plugnmeet-protocol`) ارسال شود؛ سرور طبق `HandleAuthHeaderCheck` هدرهای امنیتی را بررسی می‌کند. ## گام ۱: ایجاد یا فعال بودن اتاق ### API Endpoint برای Django Backend: ``` POST /api/courses//online/room/create/ ``` ### بدنه درخواست از فرانت به Django: ```json { "subject": "کلاس جبر فصل ۱" // اختیاری - عنوان روم } ``` **⚠️ نکات مهم:** - **فرانت نباید `metadata` ارسال کند!** - بک‌اند Django (در `apps/course/views/live_session.py`) به‌طور خودکار تنظیمات امنیتی را اعمال می‌کند - این تضمین می‌کند که تنظیمات امنیتی به‌صورت متمرکز و یکسان اعمال شود **🎯 تنظیمات ضروری برای نمایش فیچرها:** - برای نمایش **Whiteboard**: باید `whiteboardFeatures.allowedWhiteboard: true` باشد - برای نمایش **SharedNotePad**: باید `sharedNotePadFeatures.allowedSharedNotePad: true` باشد و Etherpad service فعال باشد - برای نمایش **BreakoutRoom**: باید `breakoutRoomFeatures.isAllow: true` باشد (فقط در منوی admin) ### بدنه درخواست از Django به PlugNMeet (خودکار): بک‌اند Django این بدنه را خودش به PlugNMeet ارسال می‌کند: **⚠️ توجه به نامگذاری:** - در Python می‌توانید از `snake_case` استفاده کنید - اما **حتماً قبل از ارسال به PlugNMeet API** باید به `camelCase` تبدیل شود - مثال: `default_lock_settings` → `defaultLockSettings` - مثال: `room_features` → `roomFeatures` ```json { "room_id": "algebra-1402", "metadata": { "room_title": "کلاس جبر فصل ۱", "defaultLockSettings": { "lockMicrophone": true, // 🔒 قفل - فقط میزبان می‌تواند باز کند "lockWebcam": true, // 🔒 قفل - فقط میزبان می‌تواند باز کند "lockScreenSharing": true, // 🔒 قفل - فقط میزبان می‌تواند باز کند "lockWhiteboard": false, // ✅ همه می‌توانند ویرایش کنند "lockSharedNotepad": false, // ✅ همه می‌توانند ویرایش کنند "lockChat": false, "lockChatSendMessage": false, "lockChatFileShare": false, "lockPrivateChat": false }, "roomFeatures": { "allowWebcams": true, "muteOnStart": true, // 🔇 همه با میک خاموش وارد می‌شوند "allowScreenSharing": true, "allowRecording": true, "allowRtmp": false, "allowViewOtherWebcams": true, "allowViewOtherParticipantsList": true, "adminOnlyWebcams": false, "allowPolls": true, "roomDuration": 0, "chatFeatures": { "allowChat": true, "allowFileUpload": true }, "sharedNotePadFeatures": { "allowedSharedNotePad": true }, "whiteboardFeatures": { "allowedWhiteboard": true }, "breakoutRoomFeatures": { "isAllow": true, "allowedNumberRooms": 6 }, "waitingRoomFeatures": { "isActive": false }, "recordingFeatures": { "isAllow": true, "isAllowCloud": true, "enableAutoCloudRecording": false } } } } ``` > **چرا بک‌اند این کار را می‌کند؟** > - ✅ **امنیت متمرکز**: تنظیمات امنیتی در یک جا کنترل می‌شود > - ✅ **جلوگیری از دستکاری**: فرانت نمی‌تواند تنظیمات را تغییر دهد > - ✅ **یکپارچگی**: همه کلاس‌ها با تنظیمات یکسان ساخته می‌شوند > - 🔒 طبق تابع `AssignLockSettingsToUser` در `pkg/models/user_lock.go` این مقادیر برای کاربران غیر-admin اعمال می‌شود ## گام ۲: گرفتن توکن ورود ### API Endpoint برای Django Backend: ``` POST /api/courses/online/room/token/ ``` ### درخواست از فرانت به Django: ``` Headers: Authorization: Token Content-Type: application/json Body: { "course_slug": "algebra-10" } ``` **⚠️ نکات مهم:** - **فرانت فقط `course_slug` ارسال می‌کند!** - بک‌اند Django از `Authorization` header کاربر را شناسایی می‌کند - بک‌اند خودش live session فعال دوره را پیدا می‌کند: ```python # 1. پیدا کردن دوره course = Course.objects.get(slug=course_slug) # 2. پیدا کردن live session فعال session = CourseLiveSession.objects.get( course=course, ended_at__isnull=True # session هایی که هنوز به پایان نرسیده‌اند ) # 3. گرفتن room_id room_id = session.room_id ``` - بک‌اند خودش همه اطلاعات کاربر را می‌سازد: - `user_id` از `request.user` - `name` از `user.get_full_name()` یا `user.email` - `is_admin` از `user.can_manage_course(course)` - `profilePic` از `user.avatar` - `lock_settings` برای غیر-admin ### بدنه درخواست از Django به PlugNMeet (خودکار): بک‌اند Django این payload را خودش می‌سازد و به PlugNMeet می‌فرستد: **برای استاد:** ```json { "room_id": "algebra-1402", "user_info": { "user_id": "10", // 🔐 از request.user "name": "استاد نمونه", // 🔐 از user.get_full_name() "is_admin": true, // 🔐 از user.can_manage_course() "user_metadata": { "is_hidden": false, "profilePic": "https://..." // 🔐 از user.avatar } } } ``` **برای دانشجو:** ```json { "room_id": "algebra-1402", "user_info": { "user_id": "27", // 🔐 از request.user "name": "دانشجو نمونه", // 🔐 از user.get_full_name() "is_admin": false, // 🔐 از user.can_manage_course() "user_metadata": { "profilePic": "https://...", // 🔐 از user.avatar "lock_settings": { // 🔒 خودکار برای غیر-admin "lock_microphone": true, "lock_screen_sharing": true, "lock_webcam": true, "lock_whiteboard": false, // ✅ می‌تواند روی whiteboard بنویسد "lock_shared_notepad": false, // ✅ می‌تواند در notepad بنویسد "lock_chat": false, "lock_chat_send_message": false, "lock_chat_file_share": false, "lock_private_chat": false } } } } ``` ### نحوه کار بک‌اند Django: ```python # 1. شناسایی کاربر از token user = request.user # از Authorization header # 2. پیدا کردن دوره و session فعال course = Course.objects.get(slug=course_slug) session = CourseLiveSession.objects.get(course=course, ended_at__isnull=True) room_id = session.room_id # 3. تشخیص نقش is_admin = user.can_manage_course(course) # استاد یا مالک دوره # 4. ساخت user_info user_info = { 'user_id': str(user.id), 'name': user.get_full_name() or user.email, 'is_admin': is_admin, } # 4. اضافه کردن profilePic profile_pic = request.build_absolute_uri(user.avatar.url) user_metadata['profilePic'] = profile_pic # 5. اضافه کردن lock_settings برای غیر-admin if not is_admin: user_metadata['lock_settings'] = { 'lock_microphone': True, 'lock_screen_sharing': True, 'lock_webcam': True, 'lock_whiteboard': False, # دانشجو می‌تواند روی whiteboard بنویسد 'lock_shared_notepad': False, # دانشجو می‌تواند در notepad بنویسد 'lock_chat': False, 'lock_chat_send_message': False, 'lock_chat_file_share': False, 'lock_private_chat': False, } ``` ### ارسال به PlugNMeet: بک‌اند Django با هدرهای امنیتی به PlugNMeet ارسال می‌کند: - `API-KEY`: از settings - `HASH-SIGNATURE`: `HMAC_SHA256(body, secret)` - این توکن JWT اختصاصی plugNmeet است که در `GeneratePNMJoinToken` ساخته می‌شود - `is_admin: true` باعث می‌شود در `GetPNMJoinToken` کاربر به عنوان presenter با تمام دسترسی‌ها ثبت شود - `lock_settings` باعث می‌شود در فرانت‌اند PlugNMeet دکمه‌های میکروفون/وبکم غیرفعال شوند ### پاسخ Django به فرانت: ```json { "room_id": "algebra-1402", "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "plugnmeet": { "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "expires": 300, ... } } ``` فرانت با این `token` می‌تواند کاربر را به PlugNMeet وارد کند: ``` https://meet.newhorizonco.uk/?access_token= ``` ## گام ۳: ورود کلاینت با توکن ۱. توکن را در URL یا کوکی قرار دهید؛ کلاینت مقدار را از `access_token` در کوئری‌استرینگ یا از کوکی `pnm_access_token` می‌خواند (`getAccessToken` در `client/src/helpers/utils.ts`). ۲. آدرس ورود: `https://meet.newhorizonco.uk/?access_token=`. ۳. اپلیکیشن React موجود در `client/src/components/app/index.tsx` پس از بارگذاری: - درخواست `POST /api/verifyToken` را با هدر `Authorization: ` می‌فرستد (`HandleVerifyToken`). - اگر توکن معتبر باشد، لیست آدرس‌های NATS و موضوعات لازم را می‌گیرد و اتصال را آغاز می‌کند (`startNatsConn`). ۴. پس از اتصال، وضعیت کاربر و اتاق در Redux ذخیره می‌شود (`sessionSlice`). اگر کاربر ادمین باشد، تمام امکانات بدون محدودیت فعال است؛ در غیر این صورت مقدارهای `lock_settings` تعیین می‌کنند چه دکمه‌هایی فعال باشند. ## کنترل حالت صحبت/شنیدن برای استاد و دانشجو ### استاد (Moderator/Host): - ✅ در توکن `is_admin: true` ارسال می‌شود - ✅ بک‌اند Django در `apps/course/views/live_session.py` این را تشخیص می‌دهد: ```python is_admin = user.can_manage_course(course) # استاد یا مالک دوره ``` - ✅ سرور PlugNMeet در `GetPNMJoinToken` رول presenter را فعال می‌کند - ✅ **هیچ قفلی** روی میکروفون، وبکم یا اشتراک صفحه اعمال نمی‌شود - 🎤 استاد می‌تواند بلافاصله صحبت کند و به دانشجو **اجازه صحبت** دهد ### دانشجو (Participant): - 🔒 در توکن `is_admin: false` ارسال می‌شود - 🔒 بک‌اند Django خودکار lock_settings را اضافه می‌کند: ```python if not is_admin: user_metadata['lock_settings'] = { 'lock_microphone': True, 'lock_screen_sharing': True, 'lock_webcam': True, 'lock_whiteboard': False, # می‌تواند روی whiteboard بنویسد 'lock_shared_notepad': False, # می‌تواند در notepad بنویسد 'lock_chat': False, 'lock_chat_send_message': False, 'lock_chat_file_share': False, 'lock_private_chat': False, } ``` - 🔇 دکمه‌های میکروفون، وبکم و اشتراک صفحه **غیرفعال** هستند - 👂 فقط می‌تواند **گوش دهد** تا میزبان اجازه دهد - ✅ اما می‌تواند در **Whiteboard** و **SharedNotePad** بنویسد و چت کند - این منطق در `joinModal.tsx` با متغیر `isMicLock` پیاده‌سازی شده است ### نحوه دادن اجازه به دانشجو: - میزبان باید از داخل کلاس از طریق UI کنترل کند - یا از API `/api/updateLockSettings` یا `switchPresenter` استفاده کند ## نکات تکمیلی ### توکن‌ها و انقضا: - توکن‌ها زمان انقضای مفهومی دارند (`client.token_validity` در YAML) - در صورت نزدیک شدن به انقضا، کلاینت خودکار با `REQ_RENEW_PNM_TOKEN` درخواست تمدید می‌دهد ### Authorization: - برای درخواست‌های بعدی به `/api/...` همان هدر `Authorization` را ست کنید - کلاینت این کار را در `helpers/api/plugNmeetAPI.ts` انجام می‌دهد ### مدیریت دسترسی‌ها: - اگر می‌خواهید دانشجو را به صحبت‌کننده ارتقا دهید: `/api/updateLockSettings` یا `switchPresenter` - این کار فقط توسط **میزبان** امکان‌پذیر است ## 🔐 جمع‌بندی امنیت ### ❌ چیزهایی که فرانت نباید انجام دهد: #### موقع ساخت روم: - ❌ ارسال `metadata` - ❌ ارسال `default_lock_settings` - ❌ ارسال `room_features` #### موقع گرفتن توکن: - ❌ ارسال `room_id` (بک‌اند خودش از session فعال می‌گیرد) - ❌ ارسال `user_info` - ❌ ارسال `is_admin` - ❌ ارسال `lock_settings` - ❌ ارسال `user_id` یا `name` ### ✅ چیزهایی که فرانت فقط ارسال می‌کند: #### موقع ساخت روم: ```json { "room_id": "algebra-1402", // اختیاری "subject": "کلاس جبر" // اختیاری } ``` #### موقع گرفتن توکن: ```json { "course_slug": "algebra-10" // فقط این! } ``` + `Authorization: Token ` در header ### ✅ چیزهایی که بک‌اند Django خودش انجام می‌دهد: #### برای همه درخواست‌ها: - ✅ شناسایی کاربر از `Authorization` header - ✅ بررسی دسترسی با `user.can_manage_course()` یا `Participant.objects.filter()` #### موقع ساخت روم: - ✅ تعیین `defaultLockSettings` (همه `true` به جز whiteboard/notepad) - ✅ تعیین `roomFeatures` **کامل** شامل: - ✅ `sharedNotePadFeatures.allowedSharedNotePad: true` - ✅ `whiteboardFeatures.allowedWhiteboard: true` - ✅ `breakoutRoomFeatures.isAllow: true` - ✅ `chatFeatures`, `recordingFeatures`, و سایر فیچرها - ✅ تبدیل نام‌های `snake_case` به `camelCase` قبل از ارسال به PlugNMeet - ✅ ساخت `metadata` کامل برای PlugNMeet #### موقع گرفتن توکن: - ✅ پیدا کردن live session فعال از `course_slug` - ✅ گرفتن `room_id` از session - ✅ ساخت `user_id` از `request.user.id` - ✅ ساخت `name` از `user.get_full_name()` یا `user.email` - ✅ تشخیص `is_admin` از `user.can_manage_course(course)` - ✅ گرفتن `profilePic` از `user.avatar` - ✅ اضافه کردن `lock_settings` کامل برای غیر-admin شامل: - ✅ `lock_microphone`, `lock_webcam`, `lock_screen_sharing` (همه `True`) - ✅ `lock_whiteboard`, `lock_shared_notepad` (همه `False` - می‌توانند بنویسند) - ✅ `lock_chat`, `lock_chat_send_message`, `lock_private_chat` (همه `False`) - ✅ تبدیل نام‌های `snake_case` به `camelCase` قبل از ارسال - ✅ ساخت `user_info` کامل برای PlugNMeet **نتیجه:** - 🔒 **امنیت کامل**: فرانت نمی‌تواند هیچ تنظیمات امنیتی را دستکاری کند - ✅ **متمرکز**: همه logic در بک‌اند Django است - 🎯 **ساده**: فرانت فقط `course_slug` و `Authorization` header ارسال می‌کند - 🔐 **قابل کنترل**: بک‌اند تعیین می‌کند کدام session فعال است --- ## 🐛 عیب‌یابی ### مشکل: Whiteboard/SharedNotePad نمایش داده نمی‌شود **علائم:** - آیکون Whiteboard در footer نمایش داده نمی‌شود - گزینه Enable/Disable SharedNotePad در منوی admin نیست - گزینه Manage Breakout Room در منوی admin نیست **راه حل‌ها:** 1. **بررسی `roomFeatures` در room creation:** ```json "roomFeatures": { "sharedNotePadFeatures": { "allowedSharedNotePad": true // ✅ باید true باشد }, "whiteboardFeatures": { "allowedWhiteboard": true // ✅ باید true باشد }, "breakoutRoomFeatures": { "isAllow": true // ✅ باید true باشد } } ``` 2. **بررسی نامگذاری فیلدها:** - ❌ `shared_note_pad_features` (snake_case) - اشتباه - ✅ `sharedNotePadFeatures` (camelCase) - صحیح 3. **بررسی `config.yaml` در plugnmeet-server:** ```yaml shared_notepad: enabled: true # ✅ باید true باشد etherpad_hosts: - id: "etherpad_node_01" host: "http://plugnmeet-etherpad:9001" client_id: "plugNmeet" client_secret: "..." ``` 4. **بررسی Etherpad service:** ```bash docker ps | grep etherpad # باید یک container با نام plugnmeet-etherpad اجرا باشد ``` 5. **بررسی `defaultLockSettings`:** - اگر `lockWhiteboard: true` باشد، فقط admin می‌تواند ویرایش کند - اگر `lockSharedNotepad: true` باشد، فقط admin می‌تواند ویرایش کند 6. **بررسی user `lock_settings` در توکن:** ```json "lock_settings": { "lock_whiteboard": false, // false = می‌تواند ویرایش کند "lock_shared_notepad": false // false = می‌تواند ویرایش کند } ``` ### مشکل: دانشجو نمی‌تواند در Whiteboard بنویسد **علت:** - `lock_whiteboard: true` در توکن کاربر **راه حل:** - در هنگام ساخت توکن برای دانشجو، `lock_whiteboard` را `false` کنید - یا از منوی admin، lock را برای آن کاربر خاص باز کنید ### مشکل: SharedNotePad آیکون دارد اما باز نمی‌شود **علت:** - Etherpad service اجرا نیست یا در دسترس نیست **راه حل:** ```bash # بررسی وضعیت Etherpad docker-compose -f docker-compose.plugnmeet.yml ps etherpad # اگر اجرا نیست، راه‌اندازی کنید docker-compose -f docker-compose.plugnmeet.yml up -d plugnmeet-etherpad # بررسی logs docker-compose -f docker-compose.plugnmeet.yml logs -f plugnmeet-etherpad ```