Browse Source
feat(api): add live session room APIs and metadata
feat(api): add live session room APIs and metadata
- add CourseLiveSession.room_id field with unique index - introduce LiveSessionRoomCreateSerializer and LiveSessionTokenSerializer - add URLs for room creation and token: - <slug>/online/room/create - online/room/token - enhance token validation metadata with: - can_create_live_session, can_join_live_session flags - live session context (active_room_id, timings, details) - improve logging and token error handling in validation flow - add PlugNMeet configuration settings (URL, API key/secret, timeout) This introduces the endpoints and data structures needed to create and join live sessions, and surfaces richer metadata for frontend usage. A database migration is required for the new model field and index.master
14 changed files with 1632 additions and 28 deletions
-
22apps/course/migrations/0009_auto_20251014_0051.py
-
9apps/course/models/live_session.py
-
22apps/course/serializers/online.py
-
3apps/course/services/__init__.py
-
74apps/course/services/plugnmeet.py
-
182apps/course/tests/test_live_session_api.py
-
312apps/course/token-join-guide.md
-
2apps/course/urls.py
-
3apps/course/views/__init__.py
-
102apps/course/views/course.py
-
231apps/course/views/live_session.py
-
4config/settings/base.py
-
333docs/live-session-api.md
-
361docs/live-session-logs.md
@ -0,0 +1,22 @@ |
|||||
|
# Generated by Django 3.2.4 on 2025-10-14 00:51 |
||||
|
|
||||
|
from django.db import migrations, models |
||||
|
|
||||
|
|
||||
|
class Migration(migrations.Migration): |
||||
|
|
||||
|
dependencies = [ |
||||
|
('course', '0008_auto_20251013_1724'), |
||||
|
] |
||||
|
|
||||
|
operations = [ |
||||
|
migrations.AddField( |
||||
|
model_name='courselivesession', |
||||
|
name='room_id', |
||||
|
field=models.CharField(blank=True, help_text='Identifier of the PlugNMeet room.', max_length=255, null=True, unique=True, verbose_name='Room ID'), |
||||
|
), |
||||
|
migrations.AddIndex( |
||||
|
model_name='courselivesession', |
||||
|
index=models.Index(fields=['room_id'], name='course_cour_room_id_ed0222_idx'), |
||||
|
), |
||||
|
] |
||||
@ -0,0 +1,3 @@ |
|||||
|
from .plugnmeet import PlugNMeetClient, PlugNMeetError |
||||
|
|
||||
|
__all__ = ['PlugNMeetClient', 'PlugNMeetError'] |
||||
@ -0,0 +1,74 @@ |
|||||
|
import json |
||||
|
import hmac |
||||
|
import hashlib |
||||
|
from typing import Any, Dict, Optional |
||||
|
from urllib.parse import urljoin |
||||
|
|
||||
|
import requests |
||||
|
from django.conf import settings |
||||
|
from django.core.exceptions import ImproperlyConfigured |
||||
|
|
||||
|
|
||||
|
class PlugNMeetError(Exception): |
||||
|
def __init__(self, message: str, *, status_code: Optional[int] = None, response_data: Optional[Dict[str, Any]] = None): |
||||
|
super().__init__(message) |
||||
|
self.status_code = status_code |
||||
|
self.response_data = response_data or {} |
||||
|
|
||||
|
|
||||
|
class PlugNMeetClient: |
||||
|
def __init__(self, *, base_url: Optional[str] = None, api_key: Optional[str] = None, api_secret: Optional[str] = None, timeout: Optional[float] = None): |
||||
|
self.base_url = (base_url or getattr(settings, "PLUGNMEET_SERVER_URL", "")).rstrip("/") |
||||
|
self.api_key = api_key or getattr(settings, "PLUGNMEET_API_KEY", "") |
||||
|
self.api_secret = api_secret or getattr(settings, "PLUGNMEET_API_SECRET", "") |
||||
|
self.timeout = timeout or getattr(settings, "PLUGNMEET_TIMEOUT", 10.0) |
||||
|
|
||||
|
if not self.base_url or not self.api_key or not self.api_secret: |
||||
|
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) |
||||
|
|
||||
|
def get_join_token(self, payload: Dict[str, Any]) -> Dict[str, Any]: |
||||
|
return self._post("/auth/room/getJoinToken", payload) |
||||
|
|
||||
|
def _post(self, path: str, payload: Dict[str, Any]) -> Dict[str, Any]: |
||||
|
url = urljoin(f"{self.base_url}/", path.lstrip("/")) |
||||
|
body = json.dumps(payload, ensure_ascii=False, separators=(",", ":")) |
||||
|
headers = { |
||||
|
"Content-Type": "application/json", |
||||
|
"API-KEY": self.api_key, |
||||
|
"HASH-SIGNATURE": self._build_signature(body), |
||||
|
} |
||||
|
|
||||
|
try: |
||||
|
response = requests.post(url, data=body.encode("utf-8"), headers=headers, timeout=self.timeout) |
||||
|
except requests.RequestException as exc: |
||||
|
raise PlugNMeetError("Failed to reach PlugNMeet server.") from exc |
||||
|
|
||||
|
if response.status_code >= 400: |
||||
|
response_data = self._safe_json(response) |
||||
|
raise PlugNMeetError( |
||||
|
"PlugNMeet server returned an error.", |
||||
|
status_code=response.status_code, |
||||
|
response_data=response_data, |
||||
|
) |
||||
|
|
||||
|
data = self._safe_json(response) |
||||
|
if data is None: |
||||
|
raise PlugNMeetError("PlugNMeet server returned an invalid response format.") |
||||
|
return data |
||||
|
|
||||
|
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() |
||||
|
|
||||
|
@staticmethod |
||||
|
def _safe_json(response: requests.Response) -> Optional[Dict[str, Any]]: |
||||
|
try: |
||||
|
return response.json() |
||||
|
except ValueError: |
||||
|
return None |
||||
|
|
||||
|
|
||||
|
__all__ = ["PlugNMeetClient", "PlugNMeetError"] |
||||
@ -0,0 +1,182 @@ |
|||||
|
import tempfile |
||||
|
from unittest import mock |
||||
|
|
||||
|
from django.core.files.uploadedfile import SimpleUploadedFile |
||||
|
from django.test import override_settings |
||||
|
from django.urls import reverse |
||||
|
from django.utils import timezone |
||||
|
from rest_framework import status |
||||
|
from rest_framework.test import APITestCase |
||||
|
|
||||
|
from apps.account.models import ProfessorUser, StudentUser |
||||
|
from apps.course.models import ( |
||||
|
Course, |
||||
|
CourseCategory, |
||||
|
CourseLiveSession, |
||||
|
Participant, |
||||
|
) |
||||
|
|
||||
|
|
||||
|
@override_settings( |
||||
|
PLUGNMEET_SERVER_URL='https://meet.example.com', |
||||
|
PLUGNMEET_API_KEY='test-key', |
||||
|
PLUGNMEET_API_SECRET='test-secret', |
||||
|
MEDIA_ROOT=tempfile.gettempdir(), |
||||
|
) |
||||
|
class CourseLiveSessionAPITests(APITestCase): |
||||
|
def setUp(self): |
||||
|
self.professor = ProfessorUser.objects.create( |
||||
|
email='prof@example.com', |
||||
|
fullname='Professor Sample', |
||||
|
experience_years=5, |
||||
|
) |
||||
|
self.student = StudentUser.objects.create( |
||||
|
email='student@example.com', |
||||
|
fullname='Student Sample', |
||||
|
) |
||||
|
self.category = CourseCategory.objects.create(name='Category', slug='category') |
||||
|
thumbnail = SimpleUploadedFile('thumb.jpg', b'filecontent', content_type='image/jpeg') |
||||
|
self.course = Course.objects.create( |
||||
|
title='Sample Course', |
||||
|
slug='sample-course', |
||||
|
category=self.category, |
||||
|
professor=self.professor, |
||||
|
thumbnail=thumbnail, |
||||
|
video_type=Course.VedioTypeChoices.YOUTUBE_LINK, |
||||
|
video_link='https://example.com/video', |
||||
|
is_online=True, |
||||
|
online_link='https://example.com/live', |
||||
|
level=Course.LevelChoices.BEGINNER, |
||||
|
duration=10, |
||||
|
lessons_count=2, |
||||
|
description='Description', |
||||
|
short_description='Short', |
||||
|
status=Course.StatusChoices.ONGOING, |
||||
|
is_free=True, |
||||
|
) |
||||
|
professor_avatar = SimpleUploadedFile('prof-avatar.jpg', b'avatar', content_type='image/jpeg') |
||||
|
self.professor.avatar = professor_avatar |
||||
|
self.professor.save(update_fields=['avatar']) |
||||
|
student_avatar = SimpleUploadedFile('student-avatar.jpg', b'avatar', content_type='image/jpeg') |
||||
|
self.student.avatar = student_avatar |
||||
|
self.student.save(update_fields=['avatar']) |
||||
|
|
||||
|
@mock.patch('apps.course.views.live_session.PlugNMeetClient') |
||||
|
def test_professor_can_create_room(self, mock_client_cls): |
||||
|
mock_client = mock_client_cls.return_value |
||||
|
mock_client.create_room.return_value = {'status': 'success'} |
||||
|
|
||||
|
self.client.force_authenticate(user=self.professor) |
||||
|
url = reverse('course-live-session-room-create', kwargs={'slug': self.course.slug}) |
||||
|
payload = { |
||||
|
'room_id': 'custom-room-id', |
||||
|
'subject': 'Algebra Session', |
||||
|
} |
||||
|
response = self.client.post(url, payload, format='json') |
||||
|
|
||||
|
self.assertEqual(response.status_code, status.HTTP_201_CREATED) |
||||
|
mock_client.create_room.assert_called_once() |
||||
|
self.assertTrue( |
||||
|
CourseLiveSession.objects.filter(course=self.course, room_id='custom-room-id').exists() |
||||
|
) |
||||
|
|
||||
|
@mock.patch('apps.course.views.live_session.PlugNMeetClient') |
||||
|
def test_professor_receives_admin_token(self, mock_client_cls): |
||||
|
mock_client = mock_client_cls.return_value |
||||
|
mock_client.get_join_token.return_value = {'token': 'abc123'} |
||||
|
|
||||
|
session = CourseLiveSession.objects.create( |
||||
|
course=self.course, |
||||
|
subject='Session', |
||||
|
started_at=timezone.now(), |
||||
|
room_id='room-123', |
||||
|
) |
||||
|
|
||||
|
self.client.force_authenticate(user=self.professor) |
||||
|
url = reverse('course-live-session-token') |
||||
|
response = self.client.post(url, {'room_id': session.room_id}, format='json') |
||||
|
|
||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK) |
||||
|
args, _ = mock_client.get_join_token.call_args |
||||
|
payload = args[0] |
||||
|
self.assertTrue(payload['user_info']['is_admin']) |
||||
|
profile_pic = payload['user_info']['user_metadata'].get('profilePic') |
||||
|
self.assertEqual(profile_pic, f"http://testserver{self.professor.avatar.url}") |
||||
|
self.assertEqual(response.data['token'], 'abc123') |
||||
|
|
||||
|
@mock.patch('apps.course.views.live_session.PlugNMeetClient') |
||||
|
def test_student_participant_receives_limited_token(self, mock_client_cls): |
||||
|
mock_client = mock_client_cls.return_value |
||||
|
mock_client.get_join_token.return_value = {'token': 'student-token'} |
||||
|
|
||||
|
session = CourseLiveSession.objects.create( |
||||
|
course=self.course, |
||||
|
subject='Session', |
||||
|
started_at=timezone.now(), |
||||
|
room_id='room-456', |
||||
|
) |
||||
|
Participant.objects.create(course=self.course, student=self.student) |
||||
|
|
||||
|
self.client.force_authenticate(user=self.student) |
||||
|
url = reverse('course-live-session-token') |
||||
|
response = self.client.post(url, {'room_id': session.room_id}, format='json') |
||||
|
|
||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK) |
||||
|
args, _ = mock_client.get_join_token.call_args |
||||
|
payload = args[0] |
||||
|
self.assertFalse(payload['user_info']['is_admin']) |
||||
|
metadata = payload['user_info']['user_metadata'] |
||||
|
self.assertIn('lock_microphone', metadata['lock_settings']) |
||||
|
self.assertEqual(metadata.get('profilePic'), f"http://testserver{self.student.avatar.url}") |
||||
|
self.assertEqual(response.data['token'], 'student-token') |
||||
|
|
||||
|
def test_student_without_access_cannot_get_token(self): |
||||
|
session = CourseLiveSession.objects.create( |
||||
|
course=self.course, |
||||
|
subject='Session', |
||||
|
started_at=timezone.now(), |
||||
|
room_id='room-789', |
||||
|
) |
||||
|
|
||||
|
self.client.force_authenticate(user=self.student) |
||||
|
url = reverse('course-live-session-token') |
||||
|
response = self.client.post(url, {'room_id': session.room_id}, format='json') |
||||
|
|
||||
|
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) |
||||
|
|
||||
|
def test_validate_metadata_includes_active_room_for_student(self): |
||||
|
session = CourseLiveSession.objects.create( |
||||
|
course=self.course, |
||||
|
subject='Session Live', |
||||
|
started_at=timezone.now(), |
||||
|
room_id='room-live-1', |
||||
|
) |
||||
|
Participant.objects.create(course=self.course, student=self.student) |
||||
|
|
||||
|
self.client.force_authenticate(user=self.student) |
||||
|
url = reverse('course-online-validate', kwargs={'slug': self.course.slug}) |
||||
|
response = self.client.get(url) |
||||
|
|
||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK) |
||||
|
metadata = response.data['metadata'] |
||||
|
self.assertTrue(metadata['is_online']) |
||||
|
self.assertEqual(metadata['active_room_id'], session.room_id) |
||||
|
self.assertTrue(metadata['can_join_live_session']) |
||||
|
self.assertEqual(metadata['live_session']['room_id'], session.room_id) |
||||
|
self.assertIsNotNone(metadata['live_session']['started_at']) |
||||
|
|
||||
|
def test_validate_metadata_for_professor_hides_creation_when_online(self): |
||||
|
CourseLiveSession.objects.create( |
||||
|
course=self.course, |
||||
|
subject='Session Live', |
||||
|
started_at=timezone.now(), |
||||
|
room_id='room-live-2', |
||||
|
) |
||||
|
|
||||
|
self.client.force_authenticate(user=self.professor) |
||||
|
url = reverse('course-online-validate', kwargs={'slug': self.course.slug}) |
||||
|
response = self.client.get(url) |
||||
|
|
||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK) |
||||
|
metadata = response.data['metadata'] |
||||
|
self.assertFalse(metadata['can_create_live_session']) |
||||
@ -0,0 +1,312 @@ |
|||||
|
# راهنمای گرفتن توکن و ورود کلاینت به کلاسهای 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/<course-slug>/online/room/create/ |
||||
|
``` |
||||
|
|
||||
|
### بدنه درخواست از فرانت به Django: |
||||
|
```json |
||||
|
{ |
||||
|
"subject": "کلاس جبر فصل ۱" // اختیاری - عنوان روم |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
**⚠️ نکات مهم:** |
||||
|
- **فرانت نباید `metadata` ارسال کند!** |
||||
|
- بکاند Django (در `apps/course/views/live_session.py`) بهطور خودکار تنظیمات امنیتی را اعمال میکند |
||||
|
- این تضمین میکند که تنظیمات امنیتی بهصورت متمرکز و یکسان اعمال شود |
||||
|
|
||||
|
### بدنه درخواست از Django به PlugNMeet (خودکار): |
||||
|
بکاند Django این بدنه را خودش به PlugNMeet ارسال میکند: |
||||
|
|
||||
|
```json |
||||
|
{ |
||||
|
"room_id": "algebra-1402", |
||||
|
"metadata": { |
||||
|
"room_title": "کلاس جبر فصل ۱", |
||||
|
"default_lock_settings": { |
||||
|
"lock_microphone": true, // 🔒 قفل - فقط میزبان میتواند باز کند |
||||
|
"lock_webcam": true, // 🔒 قفل - فقط میزبان میتواند باز کند |
||||
|
"lock_screen_sharing": true // 🔒 قفل - فقط میزبان میتواند باز کند |
||||
|
}, |
||||
|
"room_features": { |
||||
|
"mute_on_start": true, // 🔇 همه با میک خاموش وارد میشوند |
||||
|
"waiting_room_features": { |
||||
|
"is_active": false |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
> **چرا بکاند این کار را میکند؟** |
||||
|
> - ✅ **امنیت متمرکز**: تنظیمات امنیتی در یک جا کنترل میشود |
||||
|
> - ✅ **جلوگیری از دستکاری**: فرانت نمیتواند تنظیمات را تغییر دهد |
||||
|
> - ✅ **یکپارچگی**: همه کلاسها با تنظیمات یکسان ساخته میشوند |
||||
|
> - 🔒 طبق تابع `AssignLockSettingsToUser` در `pkg/models/user_lock.go` این مقادیر برای کاربران غیر-admin اعمال میشود |
||||
|
|
||||
|
## گام ۲: گرفتن توکن ورود |
||||
|
|
||||
|
### API Endpoint برای Django Backend: |
||||
|
``` |
||||
|
POST /api/courses/online/room/token/ |
||||
|
``` |
||||
|
|
||||
|
### درخواست از فرانت به Django: |
||||
|
``` |
||||
|
Headers: |
||||
|
Authorization: Token <USER_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 |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
### نحوه کار بکاند 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, |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
### ارسال به 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=<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) # استاد یا مالک دوره |
||||
|
``` |
||||
|
- ✅ سرور 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, |
||||
|
} |
||||
|
``` |
||||
|
- 🔇 دکمههای میکروفون، وبکم و اشتراک صفحه **غیرفعال** هستند |
||||
|
- 👂 فقط میتواند **گوش دهد** تا میزبان اجازه دهد |
||||
|
- این منطق در `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 <USER_TOKEN>` در header |
||||
|
|
||||
|
### ✅ چیزهایی که بکاند Django خودش انجام میدهد: |
||||
|
|
||||
|
#### برای همه درخواستها: |
||||
|
- ✅ شناسایی کاربر از `Authorization` header |
||||
|
- ✅ بررسی دسترسی با `user.can_manage_course()` یا `Participant.objects.filter()` |
||||
|
|
||||
|
#### موقع ساخت روم: |
||||
|
- ✅ تعیین `default_lock_settings` (همه `true`) |
||||
|
- ✅ تعیین `room_features.mute_on_start: true` |
||||
|
- ✅ ساخت `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 |
||||
|
- ✅ ساخت `user_info` کامل برای PlugNMeet |
||||
|
|
||||
|
**نتیجه:** |
||||
|
- 🔒 **امنیت کامل**: فرانت نمیتواند هیچ تنظیمات امنیتی را دستکاری کند |
||||
|
- ✅ **متمرکز**: همه logic در بکاند Django است |
||||
|
- 🎯 **ساده**: فرانت فقط `course_slug` و `Authorization` header ارسال میکند |
||||
|
- 🔐 **قابل کنترل**: بکاند تعیین میکند کدام session فعال است |
||||
@ -1,4 +1,5 @@ |
|||||
from .course import * |
from .course import * |
||||
from .lesson import * |
from .lesson import * |
||||
from .participant import * |
from .participant import * |
||||
from .professor import * |
|
||||
|
from .professor import * |
||||
|
from .live_session import * |
||||
@ -0,0 +1,231 @@ |
|||||
|
import logging |
||||
|
|
||||
|
from django.core.exceptions import ImproperlyConfigured |
||||
|
from django.shortcuts import get_object_or_404 |
||||
|
from django.utils import timezone |
||||
|
|
||||
|
from rest_framework import status |
||||
|
from rest_framework.generics import GenericAPIView |
||||
|
from rest_framework.permissions import IsAuthenticated |
||||
|
from rest_framework.response import Response |
||||
|
|
||||
|
from apps.course.models import Course, CourseLiveSession, Participant |
||||
|
from apps.course.serializers import LiveSessionRoomCreateSerializer, LiveSessionTokenSerializer |
||||
|
from apps.course.services.plugnmeet import PlugNMeetClient, PlugNMeetError |
||||
|
from utils.exceptions import AppAPIException |
||||
|
|
||||
|
logger = logging.getLogger(__name__) |
||||
|
|
||||
|
|
||||
|
class CourseLiveSessionRoomCreateAPIView(GenericAPIView): |
||||
|
permission_classes = [IsAuthenticated] |
||||
|
serializer_class = LiveSessionRoomCreateSerializer |
||||
|
|
||||
|
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 {}) |
||||
|
serializer.is_valid(raise_exception=True) |
||||
|
|
||||
|
course = get_object_or_404(Course, slug=slug) |
||||
|
|
||||
|
if not request.user.can_manage_course(course): |
||||
|
logger.warning(f"[LiveSession Create] Permission denied - user_id={request.user.id} course={slug}") |
||||
|
raise AppAPIException({'message': 'You do not have permission to create a live session for this course.'}, status_code=status.HTTP_403_FORBIDDEN) |
||||
|
|
||||
|
logger.info(f"[LiveSession Create] Permission granted for user_id={request.user.id} course={slug}") |
||||
|
|
||||
|
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 {}) |
||||
|
|
||||
|
payload = { |
||||
|
'room_id': room_id, |
||||
|
'metadata': metadata, |
||||
|
} |
||||
|
|
||||
|
logger.info(f"[LiveSession Create] Calling PlugNMeet API - room_id={room_id} course={slug}") |
||||
|
|
||||
|
try: |
||||
|
client = PlugNMeetClient() |
||||
|
plugnmeet_response = client.create_room(payload) |
||||
|
logger.info(f"[LiveSession Create] PlugNMeet room created successfully - room_id={room_id}") |
||||
|
except ImproperlyConfigured as exc: |
||||
|
logger.error(f"[LiveSession Create] Configuration error - {str(exc)}") |
||||
|
raise AppAPIException({'message': str(exc)}, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR) |
||||
|
except PlugNMeetError as exc: |
||||
|
logger.error(f"[LiveSession Create] PlugNMeet API error - room_id={room_id} error={str(exc)}") |
||||
|
detail = exc.response_data or {'message': str(exc)} |
||||
|
status_code = exc.status_code or status.HTTP_502_BAD_GATEWAY |
||||
|
raise AppAPIException(detail, status_code=status_code) |
||||
|
|
||||
|
session, created = CourseLiveSession.objects.get_or_create( |
||||
|
course=course, |
||||
|
room_id=room_id, |
||||
|
defaults={ |
||||
|
'subject': subject, |
||||
|
'started_at': timezone.now(), |
||||
|
}, |
||||
|
) |
||||
|
|
||||
|
if created: |
||||
|
logger.info(f"[LiveSession Create] New session created - session_id={session.id} room_id={room_id} course={slug}") |
||||
|
else: |
||||
|
logger.info(f"[LiveSession Create] Existing session reactivated - session_id={session.id} room_id={room_id} course={slug}") |
||||
|
updates = {} |
||||
|
if session.subject != subject: |
||||
|
session.subject = subject |
||||
|
updates['subject'] = subject |
||||
|
if session.room_id != room_id: |
||||
|
session.room_id = room_id |
||||
|
updates['room_id'] = room_id |
||||
|
if session.started_at is None: |
||||
|
session.started_at = timezone.now() |
||||
|
updates['started_at'] = session.started_at |
||||
|
if updates: |
||||
|
session.save(update_fields=list(updates.keys())) |
||||
|
logger.info(f"[LiveSession Create] Session updated - session_id={session.id} fields={list(updates.keys())}") |
||||
|
|
||||
|
logger.info(f"[LiveSession Create] Success - session_id={session.id} room_id={room_id} course={slug} user_id={request.user.id}") |
||||
|
|
||||
|
return Response({ |
||||
|
'session': { |
||||
|
'id': session.id, |
||||
|
'room_id': session.room_id, |
||||
|
'subject': session.subject, |
||||
|
'started_at': session.started_at, |
||||
|
}, |
||||
|
'plugnmeet': plugnmeet_response, |
||||
|
}, status=status.HTTP_201_CREATED if created else status.HTTP_200_OK) |
||||
|
|
||||
|
@staticmethod |
||||
|
def _build_room_id(course: Course) -> str: |
||||
|
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 = { |
||||
|
'room_title': subject, |
||||
|
'default_lock_settings': { |
||||
|
'lock_microphone': True, |
||||
|
'lock_webcam': True, |
||||
|
'lock_screen_sharing': True, |
||||
|
}, |
||||
|
'room_features': { |
||||
|
'mute_on_start': True, |
||||
|
'waiting_room_features': { |
||||
|
'is_active': False, |
||||
|
}, |
||||
|
}, |
||||
|
} |
||||
|
return self._deep_update(base, overrides) |
||||
|
|
||||
|
def _deep_update(self, base: dict, overrides: dict) -> dict: |
||||
|
for key, value in overrides.items(): |
||||
|
if isinstance(value, dict) and isinstance(base.get(key), dict): |
||||
|
base[key] = self._deep_update(base.get(key, {}), value) |
||||
|
else: |
||||
|
base[key] = value |
||||
|
return base |
||||
|
|
||||
|
|
||||
|
class CourseLiveSessionTokenAPIView(GenericAPIView): |
||||
|
permission_classes = [IsAuthenticated] |
||||
|
serializer_class = LiveSessionTokenSerializer |
||||
|
|
||||
|
def post(self, request, *args, **kwargs): |
||||
|
serializer = self.get_serializer(data=request.data) |
||||
|
serializer.is_valid(raise_exception=True) |
||||
|
|
||||
|
course_slug = serializer.validated_data['course_slug'] |
||||
|
user = request.user |
||||
|
|
||||
|
logger.info(f"[LiveSession Token] Request from user_id={user.id} for course={course_slug}") |
||||
|
|
||||
|
try: |
||||
|
course = Course.objects.get(slug=course_slug) |
||||
|
except Course.DoesNotExist: |
||||
|
logger.warning(f"[LiveSession Token] Course not found - course={course_slug} user_id={user.id}") |
||||
|
raise AppAPIException({'message': 'Course not found.'}, status_code=status.HTTP_404_NOT_FOUND) |
||||
|
|
||||
|
if not course.is_online: |
||||
|
logger.warning(f"[LiveSession Token] Course not configured for online - course={course_slug} user_id={user.id}") |
||||
|
raise AppAPIException({'message': 'Course is not configured for online sessions.'}, status_code=status.HTTP_400_BAD_REQUEST) |
||||
|
|
||||
|
try: |
||||
|
session = CourseLiveSession.objects.select_related('course').get( |
||||
|
course=course, |
||||
|
ended_at__isnull=True |
||||
|
) |
||||
|
logger.info(f"[LiveSession Token] Active session found - session_id={session.id} room_id={session.room_id} course={course_slug}") |
||||
|
except CourseLiveSession.DoesNotExist: |
||||
|
logger.warning(f"[LiveSession Token] No active session found - course={course_slug} user_id={user.id}") |
||||
|
raise AppAPIException({'message': 'No active live session found for this course.'}, status_code=status.HTTP_404_NOT_FOUND) |
||||
|
|
||||
|
room_id = session.room_id |
||||
|
|
||||
|
is_admin = user.can_manage_course(course) |
||||
|
user_role = "professor" if is_admin else "student" |
||||
|
logger.info(f"[LiveSession Token] User role determined - user_id={user.id} role={user_role} course={course_slug}") |
||||
|
|
||||
|
if not is_admin and not Participant.objects.filter(course=course, student_id=user.id, is_active=True).exists(): |
||||
|
logger.warning(f"[LiveSession Token] Access denied - user_id={user.id} not enrolled in course={course_slug}") |
||||
|
raise AppAPIException({'message': 'You do not have access to this live session.'}, status_code=status.HTTP_403_FORBIDDEN) |
||||
|
|
||||
|
user_info = { |
||||
|
'user_id': str(user.id), |
||||
|
'name': user.get_full_name() or user.email or user.username or f"user-{user.id}", |
||||
|
'is_admin': is_admin, |
||||
|
} |
||||
|
|
||||
|
user_metadata = {} |
||||
|
profile_pic = self._build_profile_url(request, user) |
||||
|
if profile_pic: |
||||
|
user_metadata['profilePic'] = profile_pic |
||||
|
|
||||
|
if not is_admin: |
||||
|
user_metadata['lock_settings'] = { |
||||
|
'lock_microphone': True, |
||||
|
'lock_screen_sharing': True, |
||||
|
'lock_webcam': True, |
||||
|
} |
||||
|
else: |
||||
|
user_metadata['is_hidden'] = False |
||||
|
|
||||
|
if user_metadata: |
||||
|
user_info['user_metadata'] = user_metadata |
||||
|
|
||||
|
payload = { |
||||
|
'room_id': room_id, |
||||
|
'user_info': user_info, |
||||
|
} |
||||
|
|
||||
|
logger.info(f"[LiveSession Token] Requesting token from PlugNMeet - room_id={room_id} user_id={user.id} role={user_role}") |
||||
|
|
||||
|
try: |
||||
|
client = PlugNMeetClient() |
||||
|
plugnmeet_response = client.get_join_token(payload) |
||||
|
logger.info(f"[LiveSession Token] Token generated successfully - room_id={room_id} user_id={user.id}") |
||||
|
except ImproperlyConfigured as exc: |
||||
|
logger.error(f"[LiveSession Token] Configuration error - {str(exc)}") |
||||
|
raise AppAPIException({'message': str(exc)}, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR) |
||||
|
except PlugNMeetError as exc: |
||||
|
logger.error(f"[LiveSession Token] PlugNMeet API error - room_id={room_id} user_id={user.id} error={str(exc)}") |
||||
|
detail = exc.response_data or {'message': str(exc)} |
||||
|
status_code = exc.status_code or status.HTTP_502_BAD_GATEWAY |
||||
|
raise AppAPIException(detail, status_code=status_code) |
||||
|
|
||||
|
logger.info(f"[LiveSession Token] Success - room_id={room_id} user_id={user.id} role={user_role} course={course_slug}") |
||||
|
|
||||
|
return Response({ |
||||
|
'room_id': room_id, |
||||
|
'token': plugnmeet_response.get('token'), |
||||
|
'plugnmeet': plugnmeet_response, |
||||
|
}) |
||||
|
|
||||
|
@staticmethod |
||||
|
def _build_profile_url(request, user): |
||||
|
avatar = getattr(user, 'avatar', None) |
||||
|
if avatar and getattr(avatar, 'url', None): |
||||
|
return request.build_absolute_uri(avatar.url) |
||||
|
return None |
||||
@ -0,0 +1,333 @@ |
|||||
|
# راهنمای اتصال فرانتاند به API لایو کلاس |
||||
|
|
||||
|
این مستند جریان کامل «ایجاد روم لایو»، «گرفتن توکن ورود» و «مدیریت ورود استاد و دانشجو» را توضیح میدهد. |
||||
|
|
||||
|
## ۱. اعتبارسنجی وضعیت دوره |
||||
|
|
||||
|
``` |
||||
|
GET /api/courses/<course-slug>/online/validate/ |
||||
|
Headers: |
||||
|
Authorization: Token <USER_TOKEN> |
||||
|
``` |
||||
|
|
||||
|
### پاسخ نمونه (استاد، کلاس آنلاین در حال اجرا) |
||||
|
|
||||
|
```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 |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
- `can_create_live_session`: اگر `true` → استاد میتواند روم جدید بسازد (فقط وقتی کلاس آفلاین است) |
||||
|
- `can_join_live_session`: اگر `true` → کاربر میتواند به کلاس فعال بپیوندد (استاد یا دانشجو) |
||||
|
- `active_room_id`: room_id کلاس فعال (برای نمایش در UI) |
||||
|
- `livesession_started_at`: زمان شروع - برای محاسبه مدت سپریشده |
||||
|
|
||||
|
### پاسخ نمونه (استاد، کلاس آفلاین) |
||||
|
|
||||
|
```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 |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
### پاسخ نمونه (دانشجو ثبتنامکرده، کلاس آنلاین) |
||||
|
|
||||
|
```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 |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
### پاسخ نمونه (دانشجو ثبتنامکرده، کلاس آفلاین) |
||||
|
|
||||
|
```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 |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
### پاسخ نمونه (کاربر بدون دسترسی) |
||||
|
|
||||
|
```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." } |
||||
|
] |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
## ۲. ساخت یا فعال کردن روم (استاد) |
||||
|
|
||||
|
``` |
||||
|
POST /api/courses/<course-slug>/online/room/create/ |
||||
|
Headers: |
||||
|
Authorization: Token <USER_TOKEN> |
||||
|
Content-Type: application/json |
||||
|
|
||||
|
Body (نمونه): |
||||
|
{ |
||||
|
"subject": "کلاس جبر فصل ۱" // اختیاری؛ پیشفرض عنوان دوره + "Live Session" |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
**⚠️ نکات مهم:** |
||||
|
- **فرانت نباید `metadata` ارسال کند!** |
||||
|
- بکاند بهطور خودکار تنظیمات امنیتی را اعمال میکند: |
||||
|
- `lock_microphone: true` - میکروفون برای همه قفل است |
||||
|
- `lock_webcam: true` - وبکم برای همه قفل است |
||||
|
- `lock_screen_sharing: true` - اشتراک صفحه برای همه قفل است |
||||
|
- `mute_on_start: true` - همه با میکروفون خاموش وارد میشوند |
||||
|
- **فقط میزبان (استاد)** میتواند این محدودیتها را برداشته و به دانشجو اجازه دهد |
||||
|
|
||||
|
### پاسخ موفق (۲۰۱ یا ۲۰۰) |
||||
|
|
||||
|
```json |
||||
|
{ |
||||
|
"session": { |
||||
|
"id": 7, |
||||
|
"room_id": "algebra-1402", |
||||
|
"subject": "کلاس جبر فصل ۱", |
||||
|
"started_at": "2025-10-14T01:32:45+03:30" |
||||
|
}, |
||||
|
"plugnmeet": { |
||||
|
"status": "success", |
||||
|
"room_id": "algebra-1402", |
||||
|
"...": "پاسخ کامل PlugNMeet" |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
## ۳. گرفتن توکن ورود به روم |
||||
|
|
||||
|
``` |
||||
|
POST /api/courses/online/room/token/ |
||||
|
Headers: |
||||
|
Authorization: Token <USER_TOKEN> |
||||
|
Content-Type: application/json |
||||
|
|
||||
|
Body: |
||||
|
{ |
||||
|
"course_slug": "algebra-10" |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
**⚠️ نکات مهم:** |
||||
|
- **فرانت فقط `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` خودکار برای دانشجو (همه قفل) |
||||
|
|
||||
|
### پاسخ موفق |
||||
|
|
||||
|
```json |
||||
|
{ |
||||
|
"room_id": "algebra-1402", |
||||
|
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", |
||||
|
"plugnmeet": { |
||||
|
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", |
||||
|
"expires": 300, |
||||
|
"...": "پاسخ کامل PlugNMeet" |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
### نحوه استفاده: |
||||
|
- `token` باید در URL سرویس PlugNMeet استفاده شود: |
||||
|
``` |
||||
|
https://meet.newhorizonco.uk/?access_token=<token> |
||||
|
``` |
||||
|
- بکاند خودکار تشخیص میدهد: |
||||
|
- **استاد**: `is_admin: true` → همه دسترسیها بدون محدودیت |
||||
|
- **دانشجو**: `is_admin: false` + `lock_settings` → میکروفون، وبکم و اشتراک صفحه قفل است |
||||
|
|
||||
|
## سناریوهای پیشنهادی برای پیادهسازی فرانت |
||||
|
|
||||
|
### سناریوی استاد |
||||
|
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", // اختیاری |
||||
|
"subject": "کلاس جبر" // اختیاری |
||||
|
} |
||||
|
``` |
||||
|
**نکته**: فقط این دو فیلد! بکاند خودش `metadata` و تنظیمات امنیتی را اعمال میکند. |
||||
|
|
||||
|
3. **گرفتن توکن**: |
||||
|
```json |
||||
|
POST /online/room/token/ |
||||
|
Body: { |
||||
|
"course_slug": "algebra-10" // فقط این! |
||||
|
} |
||||
|
``` |
||||
|
**نکته**: بکاند خودش live session فعال را پیدا میکند و `room_id` را میگیرد. |
||||
|
|
||||
|
4. **ورود به کلاس**: |
||||
|
```javascript |
||||
|
window.open(`https://meet.newhorizonco.uk/?access_token=${token}`, "_blank"); |
||||
|
``` |
||||
|
|
||||
|
### سناریوی دانشجو |
||||
|
1. **دریافت وضعیت**: با `GET /online/validate/` وضعیت را بگیرید. |
||||
|
- اگر `is_online = true` و `can_join_live_session = true` → دکمه «ورود به کلاس» را نمایش دهید. |
||||
|
|
||||
|
2. **گرفتن توکن و ورود**: |
||||
|
```json |
||||
|
POST /online/room/token/ |
||||
|
Body: { |
||||
|
"course_slug": "algebra-10" // فقط این! |
||||
|
} |
||||
|
``` |
||||
|
سپس با `token` دریافتی به PlugNMeet وارد شوید. |
||||
|
|
||||
|
### ✅ آنچه بکاند خودکار انجام میدهد: |
||||
|
|
||||
|
#### موقع ساخت روم: |
||||
|
- ✅ تعیین `default_lock_settings` (همه `true`) |
||||
|
- ✅ تعیین `room_features.mute_on_start: true` |
||||
|
- ✅ ساخت `metadata` کامل برای PlugNMeet |
||||
|
|
||||
|
#### موقع گرفتن توکن: |
||||
|
- ✅ پیدا کردن 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` استفاده کرده و در فرانت اختلاف با زمان فعلی را محاسبه کنید |
||||
@ -0,0 +1,361 @@ |
|||||
|
# راهنمای لاگهای Live Session API |
||||
|
|
||||
|
این مستند توضیح میدهد که هر API چه لاگهایی تولید میکند و چگونه میتوان از آنها برای debug استفاده کرد. |
||||
|
|
||||
|
## 📋 فرمت لاگها |
||||
|
|
||||
|
همه لاگها با یک prefix مشخص شروع میشوند: |
||||
|
- `[LiveSession Create]` - مربوط به ساخت روم |
||||
|
- `[LiveSession Token]` - مربوط به گرفتن توکن ورود |
||||
|
- `[Online Validate]` - مربوط به اعتبارسنجی وضعیت دوره |
||||
|
|
||||
|
## 🔹 API: ساخت روم (Create Room) |
||||
|
|
||||
|
**Endpoint:** `POST /api/courses/<course-slug>/online/room/create/` |
||||
|
|
||||
|
### جریان لاگها: |
||||
|
|
||||
|
1. **شروع درخواست:** |
||||
|
``` |
||||
|
INFO [LiveSession Create] Request from user_id=10 for course=algebra-10 |
||||
|
``` |
||||
|
|
||||
|
2. **بررسی دسترسی:** |
||||
|
- **موفق:** |
||||
|
``` |
||||
|
INFO [LiveSession Create] Permission granted for user_id=10 course=algebra-10 |
||||
|
``` |
||||
|
- **رد شده:** |
||||
|
``` |
||||
|
WARNING [LiveSession Create] Permission denied - user_id=27 course=algebra-10 |
||||
|
``` |
||||
|
|
||||
|
3. **فراخوانی PlugNMeet:** |
||||
|
``` |
||||
|
INFO [LiveSession Create] Calling PlugNMeet API - room_id=algebra-10-20231014102530 course=algebra-10 |
||||
|
``` |
||||
|
|
||||
|
4. **نتیجه PlugNMeet:** |
||||
|
- **موفق:** |
||||
|
``` |
||||
|
INFO [LiveSession Create] PlugNMeet room created successfully - room_id=algebra-10-20231014102530 |
||||
|
``` |
||||
|
- **خطای پیکربندی:** |
||||
|
``` |
||||
|
ERROR [LiveSession Create] Configuration error - PLUGNMEET_API_KEY is not configured |
||||
|
``` |
||||
|
- **خطای API:** |
||||
|
``` |
||||
|
ERROR [LiveSession Create] PlugNMeet API error - room_id=algebra-10-20231014102530 error=Room already exists |
||||
|
``` |
||||
|
|
||||
|
5. **ذخیره در دیتابیس:** |
||||
|
- **Session جدید:** |
||||
|
``` |
||||
|
INFO [LiveSession Create] New session created - session_id=7 room_id=algebra-10-20231014102530 course=algebra-10 |
||||
|
``` |
||||
|
- **Session موجود:** |
||||
|
``` |
||||
|
INFO [LiveSession Create] Existing session reactivated - session_id=7 room_id=algebra-10-20231014102530 course=algebra-10 |
||||
|
INFO [LiveSession Create] Session updated - session_id=7 fields=['subject', 'started_at'] |
||||
|
``` |
||||
|
|
||||
|
6. **نتیجه نهایی:** |
||||
|
``` |
||||
|
INFO [LiveSession Create] Success - session_id=7 room_id=algebra-10-20231014102530 course=algebra-10 user_id=10 |
||||
|
``` |
||||
|
|
||||
|
### سناریوهای خطا: |
||||
|
|
||||
|
| خطا | لاگ | دلیل | |
||||
|
|-----|-----|------| |
||||
|
| Permission Denied | `WARNING [LiveSession Create] Permission denied - user_id=27 course=algebra-10` | کاربر استاد نیست | |
||||
|
| Configuration Error | `ERROR [LiveSession Create] Configuration error - ...` | تنظیمات PlugNMeet ناقص | |
||||
|
| PlugNMeet API Error | `ERROR [LiveSession Create] PlugNMeet API error - ...` | سرویس PlugNMeet پاسخ خطا داده | |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## 🔹 API: گرفتن توکن (Get Join Token) |
||||
|
|
||||
|
**Endpoint:** `POST /api/courses/online/room/token/` |
||||
|
|
||||
|
### جریان لاگها: |
||||
|
|
||||
|
1. **شروع درخواست:** |
||||
|
``` |
||||
|
INFO [LiveSession Token] Request from user_id=27 for course=algebra-10 |
||||
|
``` |
||||
|
|
||||
|
2. **پیدا کردن دوره:** |
||||
|
- **موفق:** |
||||
|
``` |
||||
|
(بدون لاگ - به مرحله بعد میرود) |
||||
|
``` |
||||
|
- **پیدا نشد:** |
||||
|
``` |
||||
|
WARNING [LiveSession Token] Course not found - course=algebra-10 user_id=27 |
||||
|
``` |
||||
|
|
||||
|
3. **بررسی تنظیمات دوره:** |
||||
|
- **دوره آنلاین نیست:** |
||||
|
``` |
||||
|
WARNING [LiveSession Token] Course not configured for online - course=algebra-10 user_id=27 |
||||
|
``` |
||||
|
|
||||
|
4. **پیدا کردن Session فعال:** |
||||
|
- **موفق:** |
||||
|
``` |
||||
|
INFO [LiveSession Token] Active session found - session_id=7 room_id=algebra-10-20231014102530 course=algebra-10 |
||||
|
``` |
||||
|
- **پیدا نشد:** |
||||
|
``` |
||||
|
WARNING [LiveSession Token] No active session found - course=algebra-10 user_id=27 |
||||
|
``` |
||||
|
|
||||
|
5. **تشخیص نقش کاربر:** |
||||
|
``` |
||||
|
INFO [LiveSession Token] User role determined - user_id=27 role=student course=algebra-10 |
||||
|
``` |
||||
|
یا |
||||
|
``` |
||||
|
INFO [LiveSession Token] User role determined - user_id=10 role=professor course=algebra-10 |
||||
|
``` |
||||
|
|
||||
|
6. **بررسی دسترسی (برای دانشجو):** |
||||
|
- **ثبتنام نشده:** |
||||
|
``` |
||||
|
WARNING [LiveSession Token] Access denied - user_id=27 not enrolled in course=algebra-10 |
||||
|
``` |
||||
|
|
||||
|
7. **درخواست توکن از PlugNMeet:** |
||||
|
``` |
||||
|
INFO [LiveSession Token] Requesting token from PlugNMeet - room_id=algebra-10-20231014102530 user_id=27 role=student |
||||
|
``` |
||||
|
|
||||
|
8. **نتیجه PlugNMeet:** |
||||
|
- **موفق:** |
||||
|
``` |
||||
|
INFO [LiveSession Token] Token generated successfully - room_id=algebra-10-20231014102530 user_id=27 |
||||
|
``` |
||||
|
- **خطا:** |
||||
|
``` |
||||
|
ERROR [LiveSession Token] Configuration error - PLUGNMEET_API_KEY is not configured |
||||
|
``` |
||||
|
یا |
||||
|
``` |
||||
|
ERROR [LiveSession Token] PlugNMeet API error - room_id=algebra-10-20231014102530 user_id=27 error=Room not found |
||||
|
``` |
||||
|
|
||||
|
9. **نتیجه نهایی:** |
||||
|
``` |
||||
|
INFO [LiveSession Token] Success - room_id=algebra-10-20231014102530 user_id=27 role=student course=algebra-10 |
||||
|
``` |
||||
|
|
||||
|
### سناریوهای خطا: |
||||
|
|
||||
|
| خطا | لاگ | دلیل | |
||||
|
|-----|-----|------| |
||||
|
| Course Not Found | `WARNING [LiveSession Token] Course not found - ...` | slug اشتباه است | |
||||
|
| Course Not Online | `WARNING [LiveSession Token] Course not configured for online - ...` | دوره آنلاین نیست | |
||||
|
| No Active Session | `WARNING [LiveSession Token] No active session found - ...` | هیچ session فعالی وجود ندارد | |
||||
|
| Access Denied | `WARNING [LiveSession Token] Access denied - user_id=X not enrolled in course=Y` | دانشجو در کلاس ثبتنام نکرده | |
||||
|
| PlugNMeet Error | `ERROR [LiveSession Token] PlugNMeet API error - ...` | مشکل در سرویس PlugNMeet | |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## 🔹 API: اعتبارسنجی (Online Validate) |
||||
|
|
||||
|
**Endpoint:** `GET /api/courses/<course-slug>/online/validate/` |
||||
|
|
||||
|
### جریان لاگها: |
||||
|
|
||||
|
1. **شروع درخواست:** |
||||
|
``` |
||||
|
INFO [Online Validate] Request received |
||||
|
``` |
||||
|
|
||||
|
2. **Decode توکن:** |
||||
|
- **موفق:** |
||||
|
``` |
||||
|
INFO [Online Validate] Token decoded successfully |
||||
|
``` |
||||
|
- **خطا:** |
||||
|
``` |
||||
|
ERROR [Online Validate] Token decode failed - error=Token has expired |
||||
|
``` |
||||
|
|
||||
|
3. **بررسی Payload:** |
||||
|
- **نامعتبر:** |
||||
|
``` |
||||
|
WARNING [Online Validate] Invalid token payload - course_id=None user_id=10 |
||||
|
``` |
||||
|
|
||||
|
4. **شروع پردازش:** |
||||
|
``` |
||||
|
INFO [Online Validate] Processing for user_id=10 course_id=42 |
||||
|
``` |
||||
|
|
||||
|
5. **پیدا کردن دوره:** |
||||
|
``` |
||||
|
INFO [Online Validate] Course found - slug=algebra-10 is_online=True |
||||
|
``` |
||||
|
|
||||
|
6. **ساخت Metadata:** |
||||
|
``` |
||||
|
DEBUG [Online Validate Metadata] user_id=10 course=algebra-10 can_manage=True is_online=True can_join=True |
||||
|
``` |
||||
|
|
||||
|
7. **نتیجه نهایی:** |
||||
|
``` |
||||
|
INFO [Online Validate] Success - user_id=10 course=algebra-10 can_create=False can_join=True |
||||
|
``` |
||||
|
|
||||
|
### سناریوهای خطا: |
||||
|
|
||||
|
| خطا | لاگ | دلیل | |
||||
|
|-----|-----|------| |
||||
|
| Token Decode Error | `ERROR [Online Validate] Token decode failed - ...` | توکن منقضی یا نامعتبر | |
||||
|
| Invalid Payload | `WARNING [Online Validate] Invalid token payload - ...` | payload توکن ناقص است | |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## 🔍 نحوه استفاده از لاگها برای Debug |
||||
|
|
||||
|
### 1. بررسی جریان کامل یک درخواست: |
||||
|
|
||||
|
برای دنبال کردن یک درخواست خاص، بر اساس `user_id` یا `course` فیلتر کنید: |
||||
|
|
||||
|
```bash |
||||
|
# برای user خاص |
||||
|
tail -f /var/log/app.log | grep "user_id=27" |
||||
|
|
||||
|
# برای course خاص |
||||
|
tail -f /var/log/app.log | grep "course=algebra-10" |
||||
|
|
||||
|
# برای room خاص |
||||
|
tail -f /var/log/app.log | grep "room_id=algebra-10-20231014102530" |
||||
|
``` |
||||
|
|
||||
|
### 2. پیدا کردن خطاها: |
||||
|
|
||||
|
```bash |
||||
|
# همه خطاهای LiveSession |
||||
|
tail -f /var/log/app.log | grep -E "\[LiveSession (Create|Token)\]" | grep ERROR |
||||
|
|
||||
|
# خطاهای PlugNMeet |
||||
|
tail -f /var/log/app.log | grep "PlugNMeet API error" |
||||
|
|
||||
|
# خطاهای دسترسی |
||||
|
tail -f /var/log/app.log | grep -E "(Permission denied|Access denied)" |
||||
|
``` |
||||
|
|
||||
|
### 3. بررسی وضعیت سیستم: |
||||
|
|
||||
|
```bash |
||||
|
# همه درخواستهای ساخت روم |
||||
|
tail -f /var/log/app.log | grep "\[LiveSession Create\] Request" |
||||
|
|
||||
|
# همه درخواستهای توکن |
||||
|
tail -f /var/log/app.log | grep "\[LiveSession Token\] Request" |
||||
|
|
||||
|
# تشخیص نقش کاربران |
||||
|
tail -f /var/log/app.log | grep "User role determined" |
||||
|
``` |
||||
|
|
||||
|
### 4. آمار سریع: |
||||
|
|
||||
|
```bash |
||||
|
# تعداد room های ساخته شده امروز |
||||
|
grep "LiveSession Create.*Success" /var/log/app.log | grep "$(date +%Y-%m-%d)" | wc -l |
||||
|
|
||||
|
# تعداد توکنهای صادر شده |
||||
|
grep "LiveSession Token.*Success" /var/log/app.log | grep "$(date +%Y-%m-%d)" | wc -l |
||||
|
|
||||
|
# تعداد خطاها |
||||
|
grep "ERROR.*LiveSession" /var/log/app.log | grep "$(date +%Y-%m-%d)" | wc -l |
||||
|
``` |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## ⚙️ تنظیمات Logging |
||||
|
|
||||
|
برای فعال کردن لاگها، در `settings.py`: |
||||
|
|
||||
|
```python |
||||
|
LOGGING = { |
||||
|
'version': 1, |
||||
|
'disable_existing_loggers': False, |
||||
|
'formatters': { |
||||
|
'verbose': { |
||||
|
'format': '{levelname} {asctime} {module} {message}', |
||||
|
'style': '{', |
||||
|
}, |
||||
|
}, |
||||
|
'handlers': { |
||||
|
'file': { |
||||
|
'level': 'INFO', |
||||
|
'class': 'logging.FileHandler', |
||||
|
'filename': '/var/log/app.log', |
||||
|
'formatter': 'verbose', |
||||
|
}, |
||||
|
'console': { |
||||
|
'level': 'INFO', |
||||
|
'class': 'logging.StreamHandler', |
||||
|
'formatter': 'verbose', |
||||
|
}, |
||||
|
}, |
||||
|
'loggers': { |
||||
|
'apps.course.views.live_session': { |
||||
|
'handlers': ['file', 'console'], |
||||
|
'level': 'INFO', |
||||
|
'propagate': False, |
||||
|
}, |
||||
|
'apps.course.views.course': { |
||||
|
'handlers': ['file', 'console'], |
||||
|
'level': 'INFO', |
||||
|
'propagate': False, |
||||
|
}, |
||||
|
}, |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
### سطوح لاگ: |
||||
|
|
||||
|
- **INFO**: جریان عادی - شروع درخواست، موفقیت عملیات |
||||
|
- **WARNING**: وضعیت غیرعادی اما قابل کنترل - دسترسی رد شده، دوره پیدا نشد |
||||
|
- **ERROR**: خطاهای سیستم - مشکل PlugNMeet، خطای پیکربندی |
||||
|
- **DEBUG**: جزئیات بیشتر - ساخت metadata، مقادیر داخلی |
||||
|
|
||||
|
برای debug بیشتر، `level` را به `DEBUG` تغییر دهید. |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## 📊 مثال: جریان کامل یک Session |
||||
|
|
||||
|
``` |
||||
|
INFO [LiveSession Create] Request from user_id=10 for course=algebra-10 |
||||
|
INFO [LiveSession Create] Permission granted for user_id=10 course=algebra-10 |
||||
|
INFO [LiveSession Create] Calling PlugNMeet API - room_id=algebra-10-20231014102530 course=algebra-10 |
||||
|
INFO [LiveSession Create] PlugNMeet room created successfully - room_id=algebra-10-20231014102530 |
||||
|
INFO [LiveSession Create] New session created - session_id=7 room_id=algebra-10-20231014102530 course=algebra-10 |
||||
|
INFO [LiveSession Create] Success - session_id=7 room_id=algebra-10-20231014102530 course=algebra-10 user_id=10 |
||||
|
|
||||
|
INFO [LiveSession Token] Request from user_id=10 for course=algebra-10 |
||||
|
INFO [LiveSession Token] Active session found - session_id=7 room_id=algebra-10-20231014102530 course=algebra-10 |
||||
|
INFO [LiveSession Token] User role determined - user_id=10 role=professor course=algebra-10 |
||||
|
INFO [LiveSession Token] Requesting token from PlugNMeet - room_id=algebra-10-20231014102530 user_id=10 role=professor |
||||
|
INFO [LiveSession Token] Token generated successfully - room_id=algebra-10-20231014102530 user_id=10 |
||||
|
INFO [LiveSession Token] Success - room_id=algebra-10-20231014102530 user_id=10 role=professor course=algebra-10 |
||||
|
|
||||
|
INFO [LiveSession Token] Request from user_id=27 for course=algebra-10 |
||||
|
INFO [LiveSession Token] Active session found - session_id=7 room_id=algebra-10-20231014102530 course=algebra-10 |
||||
|
INFO [LiveSession Token] User role determined - user_id=27 role=student course=algebra-10 |
||||
|
INFO [LiveSession Token] Requesting token from PlugNMeet - room_id=algebra-10-20231014102530 user_id=27 role=student |
||||
|
INFO [LiveSession Token] Token generated successfully - room_id=algebra-10-20231014102530 user_id=27 |
||||
|
INFO [LiveSession Token] Success - room_id=algebra-10-20231014102530 user_id=27 role=student course=algebra-10 |
||||
|
``` |
||||
|
|
||||
|
این جریان نشان میدهد: |
||||
|
1. استاد روم را ساخت |
||||
|
2. استاد توکن گرفت و وارد شد (role=professor) |
||||
|
3. دانشجو توکن گرفت و وارد شد (role=student) |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue