diff --git a/apps/course/views/live_session.py b/apps/course/views/live_session.py index c433226..3f45c9d 100644 --- a/apps/course/views/live_session.py +++ b/apps/course/views/live_session.py @@ -11,7 +11,8 @@ from rest_framework.authentication import TokenAuthentication from rest_framework.response import Response from drf_yasg.utils import swagger_auto_schema from drf_yasg import openapi - +from datetime import time +import jwt from apps.course.models import Course, CourseLiveSession, Participant, LiveSessionRecording from apps.course.serializers import LiveSessionRoomCreateSerializer, LiveSessionTokenSerializer, LiveSessionRecordedFileSerializer, LiveSessionRecordingSerializer from apps.course.services.plugnmeet import PlugNMeetClient, PlugNMeetError @@ -42,91 +43,156 @@ 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}") - - 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) + def post(self, request, slug, *args, **kwargs): + # 1. Validation and Permission Logic 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}") + raise AppAPIException({'message': 'Permission denied'}, status_code=status.HTTP_403_FORBIDDEN) - subject = serializer.validated_data.get('subject') or f"{course.title} Live Session" - room_id = self._build_room_id(course) + # 2. Build Safe Room ID (Prefix with 'room-' to satisfy NATS rules) + # OLD: f"{course.id}-imamjavad" -> "9-imamjavad" (Risk of NATS errors) + # NEW: f"room-{course.id}-imamjavad" -> "room-9-imamjavad" (Safe) + room_id = f"room-{course.id}-imamjavad" + + subject = f"{course.title} Live Session" metadata = self._build_metadata(subject) - payload = { - 'room_id': room_id, - 'metadata': metadata, - } - - logger.info(f"[LiveSession Create] Calling PlugNMeet API - room_id={room_id} course={slug}") - + # 3. Create Room via PlugNMeet API 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) - + + # Note: client.create_room only CREATES the room. + # It does not give you a token to join it. + plugnmeet_response = client.create_room({ + 'room_id': room_id, + 'metadata': metadata, + }) + except Exception as exc: + logger.error(f"PlugNMeet Error: {exc}") + raise AppAPIException({'message': str(exc)}, status_code=500) + + # 4. Save to Database session, created = CourseLiveSession.objects.get_or_create( course=course, room_id=room_id, - defaults={ - 'subject': subject, - 'started_at': timezone.now(), - }, + 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}") - + # 5. GENERATE JOIN TOKEN (Critical for User Access) + # You must fetch these exactly as they are in your config.yaml + PNM_API_KEY = "habibmeet_api_key_2024" + PNM_SECRET = "habibmeet_secret_zumyyYWqv7KR2kUqvYdq4z4sXg7XTBD2ljT6_2024" + + token_payload = { + "iss": PNM_API_KEY, # Issuer must be the API Key + "exp": int(time.time()) + 3600, # 1 hour expiry + "sub": str(request.user.id), + "room_id": room_id, + "name": f"{request.user.first_name} {request.user.last_name}", + "is_admin": True, # Gives moderator privileges + "user_id": str(request.user.id), + } + + # SIGN with the SECRET, not the key + token = jwt.encode(token_payload, PNM_SECRET, algorithm="HS256") + return Response({ + 'success': True, '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) + # Frontend uses this to connect: + # https://meet.newhorizonco.uk/?access_token={access_token} + 'access_token': token + }, status=status.HTTP_201_CREATED) + # def post(self, request, slug, *args, **kwargs): + # logger.info(f"[LiveSession Create] Request from user_id={request.user.id} for course={slug}") + + # 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) + + # 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 = self._build_room_id(course) + # metadata = self._build_metadata(subject) + + # 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: - return f"{course.id}-imamjavad" + return f"room-{course.id}-imamjavad" def _build_metadata(self, subject: str) -> dict: # Build secured, centralized metadata. Client overrides are NOT allowed.