From 8bcd7ab2913725b9db524cd39eadaadfe8045411 Mon Sep 17 00:00:00 2001 From: Abdullah Javaid Date: Sun, 19 May 2024 18:21:50 +0500 Subject: [PATCH 01/10] Add token generation frature --- backend/api/__init__.py | 2 + backend/api/utils/jaas_jwt_builder.py | 169 +++++++++++++++++++ backend/api/views/meeting.py | 40 +++++ frontend/src/components/CreateMeetingLink.js | 53 ++++-- frontend/src/utils/api.js | 10 ++ 5 files changed, 264 insertions(+), 10 deletions(-) create mode 100644 backend/api/utils/jaas_jwt_builder.py create mode 100644 backend/api/views/meeting.py diff --git a/backend/api/__init__.py b/backend/api/__init__.py index 65e0290d0..1dd4d1686 100644 --- a/backend/api/__init__.py +++ b/backend/api/__init__.py @@ -99,6 +99,7 @@ def create_app(): masters, translation, events, + meeting, ) # why blueprints http://flask.pocoo.org/docs/1.0/blueprints/ @@ -118,6 +119,7 @@ def create_app(): app.register_blueprint(mentee.mentee, url_prefix="/api/mentee") app.register_blueprint(messages.messages, url_prefix="/api/messages") app.register_blueprint(notifications.notifications, url_prefix="/api/notifications") + app.register_blueprint(meeting.meeting, url_prefix="/api/meeting") app.register_blueprint(masters.masters, url_prefix="/api/masters") app.register_blueprint(translation.translation, url_prefix="/api/translation") diff --git a/backend/api/utils/jaas_jwt_builder.py b/backend/api/utils/jaas_jwt_builder.py new file mode 100644 index 000000000..b70e5ee4f --- /dev/null +++ b/backend/api/utils/jaas_jwt_builder.py @@ -0,0 +1,169 @@ +import time, uuid +from authlib.jose import jwt + +class JaaSJwtBuilder: + """ + The JaaSJwtBuilder class helps with the generation of the JaaS JWT. + """ + + EXP_TIME_DELAY_SEC = 7200 + # Used as a delay for the exp claim value. + + NBF_TIME_DELAY_SEC = 10 + # Used as a delay for the nbf claim value. + + def __init__(self) -> None: + self.header = { 'alg' : 'RS256' } + self.userClaims = {} + self.featureClaims = {} + self.payloadClaims = {} + + def withDefaults(self): + """Returns the JaaSJwtBuilder with default valued claims.""" + return self.withExpTime(int(time.time() + JaaSJwtBuilder.EXP_TIME_DELAY_SEC)) \ + .withNbfTime(int(time.time() - JaaSJwtBuilder.NBF_TIME_DELAY_SEC)) \ + .withLiveStreamingEnabled(True) \ + .withRecordingEnabled(True) \ + .withOutboundCallEnabled(True) \ + .withTranscriptionEnabled(True) \ + .withModerator(True) \ + .withRoomName('*') \ + .withUserId(str(uuid.uuid4())) + + def withApiKey(self, apiKey): + """ + Returns the JaaSJwtBuilder with the kid claim(apiKey) set. + + :param apiKey A string as the API Key https://jaas.8x8.vc/#/apikeys + """ + self.header['kid'] = apiKey + return self + + def withUserAvatar(self, avatarUrl): + """ + Returns the JaaSJwtBuilder with the avatar claim set. + + :param avatarUrl A string representing the url to get the user avatar. + """ + self.userClaims['avatar'] = avatarUrl + return self + + def withModerator(self, isModerator): + """ + Returns the JaaSJwtBuilder with the moderator claim set. + + :param isModerator A boolean if set to True, user is moderator and False otherwise. + """ + self.userClaims['moderator'] = 'true' if isModerator == True else 'false' + return self + + def withUserName(self, userName): + """ + Returns the JaaSJwtBuilder with the name claim set. + + :param userName A string representing the user's name. + """ + self.userClaims['name'] = userName + return self + + def withUserEmail(self, userEmail): + """ + Returns the JaaSJwtBuilder with the email claim set. + + :param userEmail A string representing the user's email address. + """ + self.userClaims['email'] = userEmail + return self + + def withLiveStreamingEnabled(self, isEnabled): + """ + Returns the JaaSJwtBuilder with the livestreaming claim set. + + :param isEnabled A boolean if set to True, live streaming is enabled and False otherwise. + """ + self.featureClaims['livestreaming'] = 'true' if isEnabled == True else 'false' + return self + + def withRecordingEnabled(self, isEnabled): + """ + Returns the JaaSJwtBuilder with the recording claim set. + + :param isEnabled A boolean if set to True, recording is enabled and False otherwise. + """ + self.featureClaims['recording'] = 'true' if isEnabled == True else 'false' + return self + + def withTranscriptionEnabled(self, isEnabled): + """ + Returns the JaaSJwtBuilder with the transcription claim set. + + :param isEnabled A boolean if set to True, transcription is enabled and False otherwise. + """ + self.featureClaims['transcription'] = 'true' if isEnabled == True else 'false' + return self + + def withOutboundCallEnabled(self, isEnabled): + """ + Returns the JaaSJwtBuilder with the outbound-call claim set. + + :param isEnabled A boolean if set to True, outbound calls are enabled and False otherwise. + """ + self.featureClaims['outbound-call'] = 'true' if isEnabled == True else 'false' + return self + + def withExpTime(self, expTime): + """ + Returns the JaaSJwtBuilder with exp claim set. Use the defaults, you won't have to change this value too much. + + :param expTime Unix time in seconds since epochs plus a delay. Expiration time of the JWT. + """ + self.payloadClaims['exp'] = expTime + return self + + def withNbfTime(self, nbfTime): + """ + Returns the JaaSJwtBuilder with nbf claim set. Use the defaults, you won't have to change this value too much. + + :param nbfTime Unix time in seconds since epochs. + """ + self.payloadClaims['nbfTime'] = nbfTime + return self + + def withRoomName(self, roomName): + """ + Returns the JaaSJwtBuilder with room claim set. + + :param roomName A string representing the room to join. + """ + self.payloadClaims['room'] = roomName + return self + + def withAppID(self, AppId): + """ + Returns the JaaSJwtBuilder with the sub claim set. + + :param AppId A string representing the unique AppID (previously tenant). + """ + self.payloadClaims['sub'] = AppId + return self + + def withUserId(self, userId): + """ + Returns the JaaSJwtBuilder with the id claim set. + + :param A string representing the user, should be unique from your side. + """ + self.userClaims['id'] = userId + return self + + def signWith(self, key): + """ + Returns a signed JWT. + + :param key A string representing the private key in PEM format. + """ + context = { 'user': self.userClaims, 'features': self.featureClaims } + self.payloadClaims['context'] = context + self.payloadClaims['iss'] = 'chat' + self.payloadClaims['aud'] = 'jitsi' + return jwt.encode(self.header, self.payloadClaims, key) diff --git a/backend/api/views/meeting.py b/backend/api/views/meeting.py new file mode 100644 index 000000000..ac70e2d7c --- /dev/null +++ b/backend/api/views/meeting.py @@ -0,0 +1,40 @@ +from __future__ import print_function + +import os.path, json +from datetime import datetime, timedelta +from flask import Blueprint +from api.core import create_response +import sys, os + +from api.utils.jaas_jwt_builder import JaaSJwtBuilder + + +meeting = Blueprint("meeting", __name__) + +API_KEY = os.environ.get("EIGHT_X_EIGHT_API_KEY") +APP_ID = os.environ.get("EIGHT_X_EIGHT_APP_ID") + +@meeting.route("/generateToken", methods=["GET"]) +def generateToken(): + try: + scriptDir = os.path.dirname('/') + fp = os.path.join(scriptDir, 'rsa-private.pem') + + with open(fp, 'r') as reader: + PRIVATE_KEY = reader.read() + jaasJwt = JaaSJwtBuilder() + token = jaasJwt.withDefaults() \ + .withApiKey(API_KEY) \ + .withUserName("Zeeshan") \ + .withUserEmail("m.zeeshanasghar101@gmail.com") \ + .withModerator(True) \ + .withAppID(APP_ID) \ + .withUserAvatar("https://asda.com/avatar") \ + .signWith(PRIVATE_KEY) + + print("HERE") + return create_response(data={"token": token.decode('utf-8'), "appID": APP_ID}) + + except Exception as error: + print(error) + return create_response(status=422, message=f'Failed to generate token for meeting') \ No newline at end of file diff --git a/frontend/src/components/CreateMeetingLink.js b/frontend/src/components/CreateMeetingLink.js index 4c123f58f..35666c1b5 100644 --- a/frontend/src/components/CreateMeetingLink.js +++ b/frontend/src/components/CreateMeetingLink.js @@ -2,17 +2,20 @@ import React, { useState } from "react"; import { Modal, Button, Input, Typography, message } from "antd"; import { CopyOutlined } from "@ant-design/icons"; import { useTranslation } from "react-i18next"; +import {generateToken} from "utils/api"; const { Title } = Typography; -function URLGeneration() { +function Meeting() { const [urlModalVisible, setUrlModalVisible] = useState(true); - const [generatedUrl, setGeneratedUrl] = useState(""); + const [generatedRoomName, setGeneratedRoomName] = useState(""); + const [generatedToken, setGeneratedToken] = useState(""); + const [AppID, setGeneratedAppID] = useState(""); const { t } = useTranslation(); const copyToClipboard = () => { try { - navigator.clipboard.writeText(generatedUrl); + navigator.clipboard.writeText(generatedRoomName); message.success(t("meeting.copyMessage")); } catch (error) { console.error(t("meeting.errorCopy"), error); @@ -20,20 +23,47 @@ function URLGeneration() { } }; - const getURL = () => { + const getRoomName = () => { try { const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; - let result = 'https://jitsi.ff3l.net/'; + let RoomName = ''; for (let i = 0; i < 10; i++) { - result += characters.charAt(Math.floor(Math.random() * characters.length)); + RoomName += characters.charAt(Math.floor(Math.random() * characters.length)); } - setGeneratedUrl(result); + setGeneratedRoomName(RoomName); } catch (error) { console.error(t("meeting.errorGenerating")); message.error(t("meeting.errorGenerating")); } }; + const joinMeeting = () => { + try { + getToken(); + // showMeetingPanel(generatedToken, AppID); + message.success(generatedToken); + message.success(AppID); + } catch (error) { + console.error("Error: ", error); + message.error("Unable to join meeting"); + } + }; + + const getToken = () => { + try { + generateToken().then(resp => { + setGeneratedToken(resp.token); + setGeneratedAppID(resp.appID); + }).catch(error => { + console.error('Error:', error); + message.error("Unable to generate token"); + }); + } catch (error) { + console.error('Error:', error); + message.error("Unable to generate token"); + } + }; + return ( <> setUrlModalVisible(false)} footer={[ - , + , , @@ -52,7 +85,7 @@ function URLGeneration() {
{t("meeting.generatedURL")}
- + , - , , - , - , +
+ , +
+ , + , +
+ /
]} >
{t("meeting.generatedURL")}
- + setEditedUrl(e.target.value)} + placeholder={t("Generate or Paste meeting link to join")} // Add placeholder text here + /> ,
- /
+
]} >
{t("meeting.generatedURL")}
setEditedUrl(e.target.value)} - placeholder={t("Generate or Paste meeting link to join")} // Add placeholder text here + value={RoomName} + onChange={(e) => setRoomName(e.target.value)} + placeholder={t("Generate or Paste Meeting Link to Join")} /> ,
,