Browse Source

feat(api)!: centralize metadata and camelCase payloads

- Remove `metadata` from LiveSessionRoomCreateSerializer and ignore any
  client-provided `metadata` for security (with warning log)
- Build secured room metadata server-side with explicit default lock
  settings (mic/webcam/screen share locked; whiteboard/notepad/chat
  unlocked) and comprehensive room features (chat, recording, breakout,
  waiting room, etc.)
- Convert all request payload keys to camelCase before calling PlugNMeet
  for both room creation and join token to match protocol requirements
- Extend non-admin user lock_settings in join token to include
  whiteboard/notepad/chat controls
- Update live-session docs and add comprehensive PlugNMeet API docs

BREAKING CHANGE: Clients must no longer send `metadata` when creating a
room; all room settings are now enforced server-side and cannot be
overridden by the client. The serializer no longer accepts `metadata`.
master
mortezaei 7 months ago
parent
commit
a2e9ce6ea8
  1. 1
      apps/course/serializers/online.py
  2. 22
      apps/course/services/plugnmeet.py
  3. 55
      apps/course/views/live_session.py
  4. 639
      docs/live-session-api.md
  5. 1660
      docs/plugnmeet_api.md

1
apps/course/serializers/online.py

@ -24,7 +24,6 @@ class OnlineClassTokenVerifySerializer(serializers.Serializer):
class LiveSessionRoomCreateSerializer(serializers.Serializer):
room_id = serializers.CharField(required=False, max_length=255, allow_blank=True)
subject = serializers.CharField(required=False, max_length=255, allow_blank=True)
metadata = serializers.DictField(required=False)
def validate_room_id(self, value: str) -> str:
return value.strip()

22
apps/course/services/plugnmeet.py

@ -27,10 +27,14 @@ class PlugNMeetClient:
raise ImproperlyConfigured("PlugNMeet integration settings are incomplete.")
def create_room(self, payload: Dict[str, Any]) -> Dict[str, Any]:
return self._post("/auth/room/create", payload)
# Convert entire payload keys to camelCase as required by PlugNMeet protocol
prepared = self._camelize_dict(payload)
return self._post("/auth/room/create", prepared)
def get_join_token(self, payload: Dict[str, Any]) -> Dict[str, Any]:
return self._post("/auth/room/getJoinToken", payload)
# Convert entire payload keys to camelCase as required by PlugNMeet protocol
prepared = self._camelize_dict(payload)
return self._post("/auth/room/getJoinToken", prepared)
def is_room_active(self, room_id: str) -> Dict[str, Any]:
return self._post("/auth/room/isRoomActive", {"roomId": room_id})
@ -71,6 +75,20 @@ class PlugNMeetClient:
return data
@staticmethod
def _snake_to_camel(key: str) -> str:
parts = key.split("_")
if not parts:
return key
return parts[0] + "".join(p.capitalize() or "" for p in parts[1:])
def _camelize_dict(self, obj: Any) -> Any:
if isinstance(obj, dict):
return {self._snake_to_camel(k): self._camelize_dict(v) for k, v in obj.items()}
if isinstance(obj, list):
return [self._camelize_dict(v) for v in obj]
return obj
def _build_signature(self, body: str) -> str:
digest = hmac.new(self.api_secret.encode("utf-8"), body.encode("utf-8"), hashlib.sha256)
return digest.hexdigest()

55
apps/course/views/live_session.py

@ -24,7 +24,12 @@ class CourseLiveSessionRoomCreateAPIView(GenericAPIView):
def post(self, request, slug, *args, **kwargs):
logger.info(f"[LiveSession Create] Request from user_id={request.user.id} for course={slug}")
serializer = self.get_serializer(data=request.data or {})
data = dict(request.data or {})
if 'metadata' in data:
logger.warning("[LiveSession Create] 'metadata' provided by client will be ignored for security reasons.")
data.pop('metadata', None)
serializer = self.get_serializer(data=data)
serializer.is_valid(raise_exception=True)
course = get_object_or_404(Course, slug=slug)
@ -37,7 +42,7 @@ class CourseLiveSessionRoomCreateAPIView(GenericAPIView):
subject = serializer.validated_data.get('subject') or f"{course.title} Live Session"
room_id = serializer.validated_data.get('room_id') or self._build_room_id(course)
metadata = self._merge_metadata(subject, serializer.validated_data.get('metadata') or {})
metadata = self._build_metadata(subject)
payload = {
'room_id': room_id,
@ -103,22 +108,56 @@ class CourseLiveSessionRoomCreateAPIView(GenericAPIView):
timestamp = timezone.now().strftime('%Y%m%d%H%M%S')
return f"{course.slug}-{timestamp}"
def _merge_metadata(self, subject: str, overrides: dict) -> dict:
base = {
def _build_metadata(self, subject: str) -> dict:
# Build secured, centralized metadata. Client overrides are NOT allowed.
return {
'room_title': subject,
'default_lock_settings': {
'lock_microphone': True,
'lock_webcam': True,
'lock_screen_sharing': True,
'lock_whiteboard': False,
'lock_shared_notepad': False,
'lock_chat': False,
'lock_chat_send_message': False,
'lock_chat_file_share': False,
'lock_private_chat': False,
},
'room_features': {
'allow_webcams': True,
'mute_on_start': True,
'allow_screen_sharing': True,
'allow_recording': True,
'allow_rtmp': False,
'allow_view_other_webcams': True,
'allow_view_other_participants_list': True,
'admin_only_webcams': False,
'allow_polls': True,
'room_duration': 0,
'chat_features': {
'allow_chat': True,
'allow_file_upload': True,
},
'shared_note_pad_features': {
'allowed_shared_note_pad': True,
},
'whiteboard_features': {
'allowed_whiteboard': True,
},
'breakout_room_features': {
'is_allow': True,
'allowed_number_rooms': 6,
},
'waiting_room_features': {
'is_active': False,
},
'recording_features': {
'is_allow': True,
'is_allow_cloud': True,
'enable_auto_cloud_recording': False,
},
},
}
return self._deep_update(base, overrides)
def _deep_update(self, base: dict, overrides: dict) -> dict:
for key, value in overrides.items():
@ -188,6 +227,12 @@ class CourseLiveSessionTokenAPIView(GenericAPIView):
'lock_microphone': True,
'lock_screen_sharing': True,
'lock_webcam': True,
'lock_whiteboard': False,
'lock_shared_notepad': False,
'lock_chat': False,
'lock_chat_send_message': False,
'lock_chat_file_share': False,
'lock_private_chat': False,
}
else:
user_metadata['is_hidden'] = False

639
docs/live-session-api.md

@ -1,247 +1,242 @@
# راهنمای اتصال فرانت‌اند به API لایو کلاس
# راهنمای گرفتن توکن و ورود کلاینت به کلاس‌های 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:
```
GET /api/courses/<course-slug>/online/validate/
Headers:
Authorization: Token <USER_TOKEN>
POST /api/courses/<course-slug>/online/room/create/
```
### پاسخ نمونه (استاد، کلاس آنلاین در حال اجرا)
### بدنه درخواست از فرانت به Django:
```json
{
"course": {
"id": 42,
"slug": "algebra-10",
"title": "کلاس جبر",
"is_online": true,
"online_link": "https://imamjavad.app/courses/algebra-10/live",
"status": "ongoing",
"professor": {
"id": 10,
"fullname": "استاد نمونه",
"slug": "ostad-nemoone",
"avatar": "https://imamjavad.app/media/users/avatars/2025/10/ostad.jpg"
}
},
"user": {
"id": 10,
"email": "prof@example.com",
"fullname": "استاد نمونه",
"avatar": "https://imamjavad.app/media/users/avatars/2025/10/ostad.jpg",
"roles": ["professor"],
"is_staff": false
},
"metadata": {
"status": "ongoing",
"has_started": true,
"has_finished": false,
"professor_in_class": false,
"can_create_live_session": false,
"can_join_live_session": true,
"scheduled_times": {
"day": "monday",
"start_time": "09:00",
"timezone": "Asia/Tehran"
},
"generated_at": "2025-10-14T01:32:45+03:30",
"validated_at": "2025-10-14T01:33:10+03:30",
"redirect_path": null,
"is_online": true,
"active_room_id": "algebra-1402",
"livesession_started_at": "2025-10-14T01:15:00+03:30",
"livesession_ended_at": null,
"live_session": {
"id": 7,
"room_id": "algebra-1402",
"subject": "کلاس جبر فصل ۱",
"started_at": "2025-10-14T01:15:00+03:30",
"ended_at": null
}
}
"subject": "کلاس جبر فصل ۱" // اختیاری - عنوان روم
}
```
- `can_create_live_session`: اگر `true` → استاد می‌تواند روم جدید بسازد (فقط وقتی کلاس آفلاین است)
- `can_join_live_session`: اگر `true` → کاربر می‌تواند به کلاس فعال بپیوندد (استاد یا دانشجو)
- `active_room_id`: room_id کلاس فعال (برای نمایش در UI)
- `livesession_started_at`: زمان شروع - برای محاسبه مدت سپری‌شده
**⚠️ نکات مهم:**
- **فرانت نباید `metadata` ارسال کند!**
- بک‌اند Django (در `apps/course/views/live_session.py`) به‌طور خودکار تنظیمات امنیتی را اعمال می‌کند
- این تضمین می‌کند که تنظیمات امنیتی به‌صورت متمرکز و یکسان اعمال شود
### پاسخ نمونه (استاد، کلاس آفلاین)
**🎯 تنظیمات ضروری برای نمایش فیچرها:**
- برای نمایش **Whiteboard**: باید `whiteboardFeatures.allowedWhiteboard: true` باشد
- برای نمایش **SharedNotePad**: باید `sharedNotePadFeatures.allowedSharedNotePad: true` باشد و Etherpad service فعال باشد
- برای نمایش **BreakoutRoom**: باید `breakoutRoomFeatures.isAllow: true` باشد (فقط در منوی admin)
```json
{
"course": { "id": 42, "title": "کلاس جبر" },
"user": { "id": 10, "fullname": "استاد نمونه" },
"metadata": {
"status": "ongoing",
"has_started": true,
"has_finished": false,
"professor_in_class": false,
"can_create_live_session": true,
"can_join_live_session": false,
"scheduled_times": { "day": "monday", "time": "09:00" },
"generated_at": "2025-10-14T01:32:45+03:30",
"validated_at": "2025-10-14T01:33:10+03:30",
"redirect_path": null,
"is_online": false,
"active_room_id": null,
"livesession_started_at": null,
"livesession_ended_at": null,
"live_session": null
}
}
```
### بدنه درخواست از Django به PlugNMeet (خودکار):
بک‌اند Django این بدنه را خودش به PlugNMeet ارسال می‌کند:
### پاسخ نمونه (دانشجو ثبت‌نام‌کرده، کلاس آنلاین)
**⚠️ توجه به نامگذاری:**
- در Python می‌توانید از `snake_case` استفاده کنید
- اما **حتماً قبل از ارسال به PlugNMeet API** باید به `camelCase` تبدیل شود
- مثال: `default_lock_settings``defaultLockSettings`
- مثال: `room_features``roomFeatures`
```json
{
"course": { "id": 42, "title": "کلاس جبر" },
"user": { "id": 27, "fullname": "دانشجو نمونه" },
"metadata": {
"status": "ongoing",
"has_started": true,
"has_finished": false,
"professor_in_class": false,
"can_create_live_session": false,
"can_join_live_session": true,
"scheduled_times": { "day": "monday", "time": "09:00" },
"generated_at": "2025-10-14T01:32:45+03:30",
"validated_at": "2025-10-14T01:33:10+03:30",
"redirect_path": null,
"is_online": true,
"active_room_id": "algebra-1402",
"livesession_started_at": "2025-10-14T01:15:00+03:30",
"livesession_ended_at": null,
"live_session": {
"id": 7,
"room_id": "algebra-1402",
"subject": "کلاس جبر فصل ۱",
"started_at": "2025-10-14T01:15:00+03:30",
"ended_at": null
"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
}
}
}
```
### پاسخ نمونه (دانشجو ثبت‌نام‌کرده، کلاس آفلاین)
```json
{
"course": { "id": 42, "title": "کلاس جبر" },
"user": { "id": 27, "fullname": "دانشجو نمونه" },
"metadata": {
"status": "ongoing",
"has_started": true,
"has_finished": false,
"professor_in_class": false,
"can_create_live_session": false,
"can_join_live_session": false,
"scheduled_times": { "day": "monday", "time": "09:00" },
"generated_at": "2025-10-14T01:32:45+03:30",
"validated_at": "2025-10-14T01:33:10+03:30",
"redirect_path": null,
"is_online": false,
"active_room_id": null,
"livesession_started_at": null,
"livesession_ended_at": null,
"live_session": null
}
}
```
### پاسخ نمونه (کاربر بدون دسترسی)
> **چرا بک‌اند این کار را می‌کند؟**
> - ✅ **امنیت متمرکز**: تنظیمات امنیتی در یک جا کنترل می‌شود
> - ✅ **جلوگیری از دستکاری**: فرانت نمی‌تواند تنظیمات را تغییر دهد
> - ✅ **یکپارچگی**: همه کلاس‌ها با تنظیمات یکسان ساخته می‌شوند
> - 🔒 طبق تابع `AssignLockSettingsToUser` در `pkg/models/user_lock.go` این مقادیر برای کاربران غیر-admin اعمال می‌شود
```json
{
"status": "error",
"code": "app_api_error",
"status_code": 403,
"message": "An error occurred while processing the request.",
"errors": [
{ "message": "You do not have access to this course." }
]
}
```
## گام ۲: گرفتن توکن ورود
## ۲. ساخت یا فعال کردن روم (استاد)
### API Endpoint برای Django Backend:
```
POST /api/courses/online/room/token/
```
### درخواست از فرانت به Django:
```
POST /api/courses/<course-slug>/online/room/create/
Headers:
Authorization: Token <USER_TOKEN>
Content-Type: application/json
Body (نمونه):
Body:
{
"subject": "کلاس جبر فصل ۱" // اختیاری؛ پیش‌فرض عنوان دوره + "Live Session"
"course_slug": "algebra-10"
}
```
**⚠️ نکات مهم:**
- **فرانت نباید `metadata` ارسال کند!**
- بک‌اند به‌طور خودکار تنظیمات امنیتی را اعمال می‌کند:
- `lock_microphone: true` - میکروفون برای همه قفل است
- `lock_webcam: true` - وبکم برای همه قفل است
- `lock_screen_sharing: true` - اشتراک صفحه برای همه قفل است
- `mute_on_start: true` - همه با میکروفون خاموش وارد می‌شوند
- **فقط میزبان (استاد)** می‌تواند این محدودیت‌ها را برداشته و به دانشجو اجازه دهد
- **فرانت فقط `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
{
"session": {
"id": 7,
"room_id": "algebra-1402",
"subject": "کلاس جبر فصل ۱",
"started_at": "2025-10-14T01:32:45+03:30"
},
"plugnmeet": {
"status": "success",
"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",
"...": "پاسخ کامل PlugNMeet"
"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
```
POST /api/courses/online/room/token/
Headers:
Authorization: Token <USER_TOKEN>
Content-Type: application/json
# 2. پیدا کردن دوره و session فعال
course = Course.objects.get(slug=course_slug)
session = CourseLiveSession.objects.get(course=course, ended_at__isnull=True)
room_id = session.room_id
Body:
{
"course_slug": "algebra-10"
# 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,
}
```
**⚠️ نکات مهم:**
- **فرانت فقط `course_slug` ارسال می‌کند!**
- بک‌اند از `Authorization` header کاربر را شناسایی می‌کند
- بک‌اند خودش live session فعال دوره را پیدا می‌کند:
```python
course = Course.objects.get(slug=course_slug)
session = CourseLiveSession.objects.get(course=course, ended_at__isnull=True)
room_id = session.room_id
```
- بک‌اند خودش همه اطلاعات کاربر را می‌سازد:
- `user_id` از `request.user.id`
- `name` از `user.get_full_name()` یا `user.email`
- `is_admin` از `user.can_manage_course(course)` - تشخیص خودکار استاد/دانشجو
- `profilePic` از `user.avatar`
- `lock_settings` خودکار برای دانشجو (همه قفل)
# 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",
@ -249,85 +244,229 @@ Body:
"plugnmeet": {
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"expires": 300,
"...": "پاسخ کامل PlugNMeet"
...
}
}
```
### نحوه استفاده:
- `token` باید در URL سرویس PlugNMeet استفاده شود:
فرانت با این `token` می‌تواند کاربر را به PlugNMeet وارد کند:
```
https://meet.newhorizonco.uk/?access_token=<TOKEN>
```
## گام ۳: ورود کلاینت با توکن
۱. توکن را در URL یا کوکی قرار دهید؛ کلاینت مقدار را از `access_token` در کوئری‌استرینگ یا از کوکی `pnm_access_token` می‌خواند (`getAccessToken` در `client/src/helpers/utils.ts`).
۲. آدرس ورود: `https://meet.newhorizonco.uk/?access_token=<TOKEN>`.
۳. اپلیکیشن React موجود در `client/src/components/app/index.tsx` پس از بارگذاری:
- درخواست `POST /api/verifyToken` را با هدر `Authorization: <TOKEN>` می‌فرستد (`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) # استاد یا مالک دوره
```
https://meet.newhorizonco.uk/?access_token=<token>
- ✅ سرور 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,
}
```
- بک‌اند خودکار تشخیص می‌دهد:
- **استاد**: `is_admin: true` → همه دسترسی‌ها بدون محدودیت
- **دانشجو**: `is_admin: false` + `lock_settings` → میکروفون، وبکم و اشتراک صفحه قفل است
- 🔇 دکمه‌های میکروفون، وبکم و اشتراک صفحه **غیرفعال** هستند
- 👂 فقط می‌تواند **گوش دهد** تا میزبان اجازه دهد
- ✅ اما می‌تواند در **Whiteboard** و **SharedNotePad** بنویسد و چت کند
- این منطق در `joinModal.tsx` با متغیر `isMicLock` پیاده‌سازی شده است
## سناریوهای پیشنهادی برای پیاده‌سازی فرانت
### نحوه دادن اجازه به دانشجو:
- میزبان باید از داخل کلاس از طریق UI کنترل کند
- یا از API `/api/updateLockSettings` یا `switchPresenter` استفاده کند
### سناریوی استاد
1. **دریافت وضعیت**: با `GET /online/validate/` وضعیت را بگیرید.
- اگر `can_create_live_session = true` است → دکمه «ساخت کلاس» را نشان دهید.
- اگر `can_join_live_session = true` است → دکمه «ورود به کلاس» را نشان دهید.
2. **ساخت روم** (در صورت نیاز):
```json
POST /online/room/create/
Body: {
"room_id": "algebra-10-1704880365000", // اختیاری
## نکات تکمیلی
### توکن‌ها و انقضا:
- توکن‌ها زمان انقضای مفهومی دارند (`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": "کلاس جبر" // اختیاری
}
```
**نکته**: فقط این دو فیلد! بک‌اند خودش `metadata` و تنظیمات امنیتی را اعمال می‌کند.
}
```
3. **گرفتن توکن**:
```json
POST /online/room/token/
Body: {
#### موقع گرفتن توکن:
```json
{
"course_slug": "algebra-10" // فقط این!
}
```
+ `Authorization: Token <USER_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: "..."
```
**نکته**: بک‌اند خودش live session فعال را پیدا می‌کند و `room_id` را می‌گیرد.
4. **ورود به کلاس**:
```javascript
window.open(`https://meet.newhorizonco.uk/?access_token=${token}`, "_blank");
4. **بررسی Etherpad service:**
```bash
docker ps | grep etherpad
# باید یک container با نام plugnmeet-etherpad اجرا باشد
```
### سناریوی دانشجو
1. **دریافت وضعیت**: با `GET /online/validate/` وضعیت را بگیرید.
- اگر `is_online = true` و `can_join_live_session = true` → دکمه «ورود به کلاس» را نمایش دهید.
5. **بررسی `defaultLockSettings`:**
- اگر `lockWhiteboard: true` باشد، فقط admin می‌تواند ویرایش کند
- اگر `lockSharedNotepad: true` باشد، فقط admin می‌تواند ویرایش کند
2. **گرفتن توکن و ورود**:
6. **بررسی user `lock_settings` در توکن:**
```json
POST /online/room/token/
Body: {
"course_slug": "algebra-10" // فقط این!
"lock_settings": {
"lock_whiteboard": false, // false = می‌تواند ویرایش کند
"lock_shared_notepad": false // false = می‌تواند ویرایش کند
}
```
سپس با `token` دریافتی به PlugNMeet وارد شوید.
### ✅ آنچه بک‌اند خودکار انجام می‌دهد:
### مشکل: دانشجو نمی‌تواند در Whiteboard بنویسد
#### موقع ساخت روم:
- ✅ تعیین `default_lock_settings` (همه `true`)
- ✅ تعیین `room_features.mute_on_start: true`
- ✅ ساخت `metadata` کامل برای PlugNMeet
**علت:**
- `lock_whiteboard: true` در توکن کاربر
#### موقع گرفتن توکن:
- ✅ پیدا کردن live session فعال از `course_slug`
- ✅ گرفتن `room_id` از session
- ✅ تشخیص `is_admin` با `user.can_manage_course(course)`
- ✅ ساخت `user_info` کامل (user_id, name, profilePic)
- ✅ اضافه کردن `lock_settings` برای دانشجو
### ❌ آنچه فرانت نباید ارسال کند:
- ❌ `metadata` موقع ساخت روم
- ❌ `room_id` موقع گرفتن توکن
- ❌ `user_info`, `is_admin`, `lock_settings`
### 🔐 نکات امنیتی:
- همه تنظیمات امنیتی در سمت سرور کنترل می‌شود
- فرانت نمی‌تواند تنظیمات را دستکاری کند
- بک‌اند تعیین می‌کند چه کسی استاد است و چه کسی دانشجو
- زمانی که استاد وارد لایو شده است، `can_create_live_session` برابر `false` می‌شود
- برای نمایش مدت سپری‌شده، از `livesession_started_at` استفاده کرده و در فرانت اختلاف با زمان فعلی را محاسبه کنید
**راه حل:**
- در هنگام ساخت توکن برای دانشجو، `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
```

1660
docs/plugnmeet_api.md
File diff suppressed because it is too large
View File

Loading…
Cancel
Save