Add jwt module to react

This commit is contained in:
Ilya Daynatovich 2017-04-21 13:00:50 +03:00 committed by Lyubo Marinov
parent 4f72225372
commit ab5c2e9ded
20 changed files with 356 additions and 222 deletions

View File

@ -273,9 +273,11 @@ function muteLocalVideo(muted) {
function maybeRedirectToWelcomePage(options) {
// if close page is enabled redirect to it, without further action
if (config.enableClosePage) {
const { isGuest } = APP.store.getState()['features/jwt'];
// save whether current user is guest or not, before navigating
// to close page
window.sessionStorage.setItem('guest', APP.tokenData.isGuest);
window.sessionStorage.setItem('guest', isGuest);
assignWindowLocationPathname('static/'
+ (options.feedbackSubmitted ? "close.html" : "close2.html"));
return;

View File

@ -1,5 +1,4 @@
/* global APP, JitsiMeetJS, config */
const logger = require("jitsi-meet-logger").getLogger(__filename);
import AuthHandler from './modules/UI/authentication/AuthHandler';
import jitsiLocalStorage from './modules/util/JitsiLocalStorage';
@ -14,6 +13,7 @@ import {
const ConnectionEvents = JitsiMeetJS.events.connection;
const ConnectionErrors = JitsiMeetJS.errors.connection;
const logger = require("jitsi-meet-logger").getLogger(__filename);
/**
* Checks if we have data to use attach instead of connect. If we have the data
@ -61,22 +61,27 @@ function checkForAttachParametersAndConnect(id, password, connection) {
* everything is ok, else error.
*/
function connect(id, password, roomName) {
let connectionConfig = Object.assign({}, config);
const connectionConfig = Object.assign({}, config);
const { issuer, jwt } = APP.store.getState()['features/jwt'];
connectionConfig.bosh += '?room=' + roomName;
let connection
= new JitsiMeetJS.JitsiConnection(null, config.token, connectionConfig);
= new JitsiMeetJS.JitsiConnection(
null,
jwt && issuer && issuer !== 'anonymous' ? jwt : undefined,
connectionConfig);
return new Promise(function (resolve, reject) {
connection.addEventListener(
ConnectionEvents.CONNECTION_ESTABLISHED, handleConnectionEstablished
);
ConnectionEvents.CONNECTION_ESTABLISHED,
handleConnectionEstablished);
connection.addEventListener(
ConnectionEvents.CONNECTION_FAILED, handleConnectionFailed
);
ConnectionEvents.CONNECTION_FAILED,
handleConnectionFailed);
connection.addEventListener(
ConnectionEvents.CONNECTION_FAILED, connectionFailedHandler);
ConnectionEvents.CONNECTION_FAILED,
connectionFailedHandler);
function connectionFailedHandler(error, errMsg) {
APP.store.dispatch(connectionFailed(connection, error, errMsg));
@ -91,12 +96,10 @@ function connect(id, password, roomName) {
function unsubscribe() {
connection.removeEventListener(
ConnectionEvents.CONNECTION_ESTABLISHED,
handleConnectionEstablished
);
handleConnectionEstablished);
connection.removeEventListener(
ConnectionEvents.CONNECTION_FAILED,
handleConnectionFailed
);
handleConnectionFailed);
}
function handleConnectionEstablished() {
@ -129,7 +132,6 @@ function connect(id, password, roomName) {
* @returns {Promise<JitsiConnection>}
*/
export function openConnection({id, password, retry, roomName}) {
let usernameOverride
= jitsiLocalStorage.getItem("xmpp_username_override");
let passwordOverride
@ -138,25 +140,20 @@ export function openConnection({id, password, retry, roomName}) {
if (usernameOverride && usernameOverride.length > 0) {
id = usernameOverride;
}
if (passwordOverride && passwordOverride.length > 0) {
password = passwordOverride;
}
return connect(id, password, roomName).catch(function (err) {
if (!retry) {
throw err;
}
return connect(id, password, roomName).catch(err => {
if (retry) {
const { issuer, jwt } = APP.store.getState()['features/jwt'];
if (err === ConnectionErrors.PASSWORD_REQUIRED) {
// do not retry if token is not valid
if (config.token) {
throw err;
} else {
if (err === ConnectionErrors.PASSWORD_REQUIRED
&& (!jwt || issuer === 'anonymous')) {
return AuthHandler.requestAuth(roomName, connect);
}
} else {
throw err;
}
throw err;
});
}

View File

@ -7,35 +7,29 @@ import {
/**
* Implements external connect using createConnectionExternally function defined
* in external_connect.js for Jitsi Meet. Parses the room name and token from
* the URL and executes createConnectionExternally.
* in external_connect.js for Jitsi Meet. Parses the room name and JSON Web
* Token (JWT) from the URL and executes createConnectionExternally.
*
* NOTE: If you are using lib-jitsi-meet without Jitsi Meet you should use this
* NOTE: If you are using lib-jitsi-meet without Jitsi Meet, you should use this
* file as reference only because the implementation is Jitsi Meet-specific.
*
* NOTE: For optimal results this file should be included right after
* external_connect.js.
*/
const hashParams = parseURLParams(window.location, true);
if (typeof createConnectionExternally === 'function') {
// URL params have higher proirity than config params.
let url
= parseURLParams(window.location, true, 'hash')[
'config.externalConnectUrl']
|| config.externalConnectUrl;
let roomName;
// URL params have higher proirity than config params.
let url = hashParams['config.externalConnectUrl'] || config.externalConnectUrl;
if (url && window.createConnectionExternally) {
const roomName = getRoomName();
if (roomName) {
if (url && (roomName = getRoomName())) {
url += `?room=${roomName}`;
let token = hashParams['config.token'] || config.token;
const token = parseURLParams(window.location, true, 'search').jwt;
if (!token) {
const searchParams
= parseURLParams(window.location, true, 'search');
token = searchParams.jwt;
}
if (token) {
url += `&token=${token}`;
}

View File

@ -362,9 +362,9 @@ UI.start = function () {
}
if(APP.tokenData.callee) {
UI.showRingOverlay();
}
const { callee } = APP.store.getState()['features/jwt'];
callee && UI.showRingOverlay();
};
/**
@ -1332,7 +1332,10 @@ UI.setMicrophoneButtonEnabled
= enabled => APP.store.dispatch(setAudioIconEnabled(enabled));
UI.showRingOverlay = function () {
RingOverlay.show(APP.tokenData.callee, interfaceConfig.DISABLE_RINGING);
const { callee } = APP.store.getState()['features/jwt'];
callee && RingOverlay.show(callee, interfaceConfig.DISABLE_RINGING);
Filmstrip.toggleFilmstrip(false, false);
};
@ -1397,7 +1400,13 @@ const UIListeners = new Map([
UI.toggleContactList
], [
UIEvents.TOGGLE_PROFILE,
() => APP.tokenData.isGuest && UI.toggleSidePanel("profile_container")
() => {
const {
isGuest
} = APP.store.getState()['features/jwt'];
isGuest && UI.toggleSidePanel('profile_container');
}
], [
UIEvents.TOGGLE_FILMSTRIP,
UI.handleToggleFilmstrip

View File

@ -1,11 +1,13 @@
/* global APP, config, JitsiMeetJS, Promise */
const logger = require("jitsi-meet-logger").getLogger(__filename);
import { openConnection } from '../../../connection';
import { setJWT } from '../../../react/features/jwt';
import UIUtil from '../util/UIUtil';
import LoginDialog from './LoginDialog';
import UIUtil from '../util/UIUtil';
import {openConnection} from '../../../connection';
const ConnectionErrors = JitsiMeetJS.errors.connection;
const logger = require("jitsi-meet-logger").getLogger(__filename);
let externalAuthWindow;
let authRequiredDialog;
@ -73,15 +75,20 @@ function redirectToTokenAuthService(roomName) {
* @param room the name fo the conference room.
*/
function initJWTTokenListener(room) {
var listener = function (event) {
if (externalAuthWindow !== event.source) {
var listener = function ({ data, source }) {
if (externalAuthWindow !== source) {
logger.warn("Ignored message not coming " +
"from external authnetication window");
return;
}
if (event.data && event.data.jwtToken) {
config.token = event.data.jwtToken;
logger.info("Received JWT token:", config.token);
let jwt;
if (data && (jwt = data.jwtToken)) {
logger.info("Received JSON Web Token (JWT):", jwt);
APP.store.dispatch(setJWT(jwt));
var roomName = room.getName();
openConnection({retry: false, roomName: roomName })
.then(function (connection) {

View File

@ -22,7 +22,8 @@ function onAvatarVisible(shown) {
*/
class RingOverlay {
/**
* @param callee instance of User class from TokenData.js
*
* @param callee The callee (Object) as defined by the JWT support.
* @param {boolean} disableRingingSound if true the ringing sound wont be played.
*/
constructor(callee, disableRingingSound) {
@ -77,9 +78,9 @@ class RingOverlay {
<div id="${this._containerId}" class='ringing' >
<div class='ringing__content'>
${callingLabel}
<img class='ringing__avatar' src="${callee.getAvatarUrl()}" />
<img class='ringing__avatar' src="${callee.avatarUrl}" />
<div class="ringing__caller-info">
<p>${callee.getName()}${callerStateLabel}</p>
<p>${callee.name}${callerStateLabel}</p>
</div>
</div>
${audioHTML}
@ -137,9 +138,12 @@ class RingOverlay {
export default {
/**
* Shows the ring overlay for the passed callee.
* @param callee {class User} the callee. Instance of User class from
* TokenData.js
* @param {boolean} disableRingingSound if true the ringing sound wont be played.
*
* @param {Object} callee - The callee. Object containing data about
* callee.
* @param {boolean} disableRingingSound - If true the ringing sound won't be
* played.
* @returns {void}
*/
show(callee, disableRingingSound = false) {
if(overlay) {

View File

@ -1,4 +1,5 @@
/* global JitsiMeetJS, config, APP */
/**
* Load the integration of a third-party analytics API such as Google
* Analytics. Since we cannot guarantee the quality of the third-party service
@ -101,26 +102,31 @@ class Analytics {
* null.
*/
init() {
let analytics = JitsiMeetJS.analytics;
if(!this.isEnabled() || !analytics)
const { analytics } = JitsiMeetJS;
if (!this.isEnabled() || !analytics)
return;
this._loadHandlers()
.then(handlers => {
let permanentProperties = {
userAgent: navigator.userAgent,
roomName: APP.conference.roomName
this._loadHandlers().then(
handlers => {
const permanentProperties = {
roomName: APP.conference.roomName,
userAgent: navigator.userAgent
};
let {server, group} = APP.tokenData;
if(server) {
const { group, server } = APP.store.getState()['features/jwt'];
if (server) {
permanentProperties.server = server;
}
if(group) {
if (group) {
permanentProperties.group = group;
}
analytics.addPermanentProperties(permanentProperties);
analytics.setAnalyticsHandlers(handlers);
}, error => analytics.dispose() && console.error(error));
},
error => analytics.dispose() && console.error(error));
}
}

View File

@ -1,123 +0,0 @@
/* global config */
/**
* Parses and handles JWT tokens. Sets config.token.
*/
import * as jws from "jws";
import { getConfigParamsFromUrl } from '../../react/features/base/config';
/**
* Get the JWT token from the URL.
*/
let params = getConfigParamsFromUrl(window.location, true, 'search');
let jwt = params.jwt;
/**
* Implements a user of conference.
*/
class User {
/**
* @param name {string} the name of the user.
* @param email {string} the email of the user.
* @param avatarUrl {string} the URL for the avatar of the user.
*/
constructor(name, email, avatarUrl) {
this._name = name;
this._email = email;
this._avatarUrl = avatarUrl;
}
/**
* GETERS START.
*/
/**
* Returns the name property
*/
getName() {
return this._name;
}
/**
* Returns the email property
*/
getEmail() {
return this._email;
}
/**
* Returns the URL of the avatar
*/
getAvatarUrl() {
return this._avatarUrl;
}
/**
* GETERS END.
*/
}
/**
* Represent the data parsed from the JWT token
*/
class TokenData{
/**
* @param {string} the JWT token
*/
constructor(jwt) {
this.isGuest = true;
if(!jwt)
return;
this.isGuest = config.enableUserRolesBasedOnToken !== true;
this.jwt = jwt;
this._decode();
// Use JWT param as token if there is not other token set and if the
// iss field is not anonymous. If you want to pass data with JWT token
// but you don't want to pass the JWT token for verification the iss
// field should be set to "anonymous"
if(!config.token && this.payload && this.payload.iss !== "anonymous")
config.token = jwt;
}
/**
* Decodes the JWT token and sets the decoded data to properties.
*/
_decode() {
this.decodedJWT = jws.decode(jwt);
if(!this.decodedJWT || !this.decodedJWT.payload)
return;
this.payload = this.decodedJWT.payload;
if(!this.payload.context)
return;
this.server = this.payload.context.server;
this.group = this.payload.context.group;
let callerData = this.payload.context.user;
let calleeData = this.payload.context.callee;
if(callerData)
this.caller = new User(callerData.name, callerData.email,
callerData.avatarUrl);
if(calleeData)
this.callee = new User(calleeData.name, calleeData.email,
calleeData.avatarUrl);
}
}
/**
* Stores the TokenData instance.
*/
let data = null;
/**
* Returns the data variable. Creates new TokenData instance if <tt>data</tt>
* variable is null.
*/
export default function getTokenData() {
if(!data)
data = new TokenData(jwt);
return data;
}

View File

@ -39,7 +39,7 @@
"jQuery-Impromptu": "trentrichardson/jQuery-Impromptu#v6.0.0",
"jquery-ui": "1.10.5",
"jssha": "1.5.0",
"jws": "3.1.4",
"jwt-decode": "2.2.0",
"lib-jitsi-meet": "jitsi/lib-jitsi-meet",
"lodash": "4.17.4",
"postis": "2.2.0",

View File

@ -1,4 +1,4 @@
import { setRoom, setRoomUrl } from '../base/conference';
import { setRoom, setRoomURL } from '../base/conference';
import { setConfig } from '../base/config';
import { getDomain, setDomain } from '../base/connection';
import { loadConfig } from '../base/lib-jitsi-meet';
@ -18,7 +18,7 @@ import {
* @returns {Function}
*/
export function appInit() {
return () => init();
return (dispatch, getState) => init(getState());
}
/**
@ -55,7 +55,7 @@ export function appNavigate(uri) {
urlObject = new URL(urlWithoutDomain, `https://${domain}`);
}
dispatch(setRoomUrl(urlObject));
dispatch(setRoomURL(urlObject));
// TODO Kostiantyn Tsaregradskyi: We should probably detect if user is
// currently in a conference and ask her if she wants to close the

View File

@ -16,7 +16,6 @@ import { WelcomePage } from '../welcome';
import KeyboardShortcut
from '../../../modules/keyboardshortcut/keyboardshortcut';
import getTokenData from '../../../modules/tokendata/TokenData';
import JitsiMeetLogStorage from '../../../modules/util/JitsiMeetLogStorage';
declare var APP: Object;
@ -111,18 +110,20 @@ export function _getRouteToRender(stateOrGetState: Object | Function) {
* Temporary solution. Later we'll get rid of global APP and set its properties
* in redux store.
*
* @param {Object} state - Snapshot of current state of redux store.
* @returns {void}
*/
export function init() {
export function init(state: Object) {
_initLogging();
APP.keyboardshortcut = KeyboardShortcut;
APP.tokenData = getTokenData();
const { jwt } = state['features/jwt'];
// Force enable the API if jwt token is passed because most probably
// jitsi meet is displayed inside of wrapper that will need to communicate
// with jitsi meet.
APP.API.init(APP.tokenData.jwt ? { forceEnable: true } : undefined);
APP.API.init(jwt ? { forceEnable: true } : undefined);
APP.translation.init();
}

View File

@ -3,4 +3,9 @@ export * from './actionTypes';
export * from './components';
export * from './functions';
// We need to import the jwt module in order to register the reducer and
// middleware, because the module is not used outside of this feature.
import '../jwt';
import './reducer';

View File

@ -1,10 +1,10 @@
/* @flow */
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { translate } from '../../i18n';
declare var APP: Object;
declare var interfaceConfig: Object;
/**
@ -131,7 +131,7 @@ class Watermarks extends Component {
let reactElement = null;
if (this.state.showJitsiWatermark
|| (APP.tokenData.isGuest
|| (this.props._isGuest
&& this.state.showJitsiWatermarkForGuests)) {
reactElement = <div className = 'watermark leftwatermark' />;
@ -175,4 +175,27 @@ class Watermarks extends Component {
}
}
export default translate(Watermarks);
/**
* Maps parts of Redux store to component prop types.
*
* @param {Object} state - Snapshot of Redux store.
* @returns {{
* _isGuest: boolean
* }}
*/
function _mapStateToProps(state) {
const { isGuest } = state['features/jwt'];
return {
/**
* The indicator which determines whether the local participant is a
* guest in the conference.
*
* @private
* @type {boolean}
*/
_isGuest: isGuest
};
}
export default connect(_mapStateToProps)(translate(Watermarks));

View File

@ -113,12 +113,13 @@ function _obtainConfigHandler() {
* @returns {void}
*/
function _setTokenData() {
const localUser = APP.tokenData.caller;
const state = APP.store.getState();
const { caller } = state['features/jwt'];
if (localUser) {
const email = localUser.getEmail();
const avatarUrl = localUser.getAvatarUrl();
const name = localUser.getName();
if (caller) {
const email = caller.email;
const avatarUrl = caller.avatarUrl;
const name = caller.name;
APP.settings.setEmail((email || '').trim(), true);
APP.settings.setAvatarUrl((avatarUrl || '').trim());

View File

@ -0,0 +1,12 @@
import { Symbol } from '../base/react';
/**
* The type of redux action which stores a specific JSON Web Token (JWT) into
* the redux store.
*
* {
* type: SET_JWT,
* jwt: string
* }
*/
export const SET_JWT = Symbol('SET_JWT');

View File

@ -0,0 +1,19 @@
/* @flow */
import { SET_JWT } from './actionTypes';
/**
* Stores a specific JSON Web Token (JWT) into the redux store.
*
* @param {string} jwt - The JSON Web Token (JWT) to store.
* @returns {{
* type: SET_TOKEN_DATA,
* jwt: string
* }}
*/
export function setJWT(jwt: string) {
return {
type: SET_JWT,
jwt
};
}

View File

@ -0,0 +1,4 @@
export * from './actions';
import './middleware';
import './reducer';

View File

@ -0,0 +1,106 @@
import jwtDecode from 'jwt-decode';
import { SET_ROOM_URL } from '../base/conference';
import { parseURLParams, SET_CONFIG } from '../base/config';
import { MiddlewareRegistry } from '../base/redux';
import { setJWT } from './actions';
import { SET_JWT } from './actionTypes';
/**
* Middleware to parse token data upon setting a new room URL.
*
* @param {Store} store - The Redux store.
* @private
* @returns {Function}
*/
MiddlewareRegistry.register(store => next => action => {
switch (action.type) {
case SET_CONFIG:
case SET_ROOM_URL:
// XXX The JSON Web Token (JWT) is not the only piece of state that we
// have decided to store in the feature jwt, there is isGuest as well
// which depends on the states of the features base/config and jwt. So
// the JSON Web Token comes from the room's URL and isGuest needs a
// recalculation upon SET_CONFIG as well.
return _setConfigOrRoomURL(store, next, action);
case SET_JWT:
return _setJWT(store, next, action);
}
return next(action);
});
/**
* Notifies the feature jwt that the action {@link SET_CONFIG} or
* {@link SET_ROOM_URL} is being dispatched within a specific Redux
* {@code store}.
*
* @param {Store} store - The Redux store in which the specified {@code action}
* is being dispatched.
* @param {Dispatch} next - The Redux dispatch function to dispatch the
* specified {@code action} to the specified {@code store}.
* @param {Action} action - The Redux action {@code SET_CONFIG} or
* {@code SET_ROOM_NAME} which is being dispatched in the specified
* {@code store}.
* @private
* @returns {Object} The new state that is the result of the reduction of the
* specified {@code action}.
*/
function _setConfigOrRoomURL({ dispatch, getState }, next, action) {
const result = next(action);
const { roomURL } = getState()['features/base/conference'];
let jwt;
if (roomURL) {
jwt = parseURLParams(roomURL, true, 'search').jwt;
}
dispatch(setJWT(jwt));
return result;
}
/**
* Notifies the feature jwt that the action {@link SET_JWT} is being dispatched
* within a specific Redux {@code store}.
*
* @param {Store} store - The Redux store in which the specified {@code action}
* is being dispatched.
* @param {Dispatch} next - The Redux dispatch function to dispatch the
* specified {@code action} to the specified {@code store}.
* @param {Action} action - The Redux action {@code SET_JWT} which is being
* dispatched in the specified {@code store}.
* @private
* @returns {Object} The new state that is the result of the reduction of the
* specified {@code action}.
*/
function _setJWT({ getState }, next, action) {
// eslint-disable-next-line no-unused-vars
const { jwt, type, ...actionPayload } = action;
if (jwt && !Object.keys(actionPayload).length) {
const {
enableUserRolesBasedOnToken
} = getState()['features/base/config'];
action.isGuest = !enableUserRolesBasedOnToken;
const jwtPayload = jwtDecode(jwt);
if (jwtPayload) {
const { context, iss } = jwtPayload;
action.issuer = iss;
if (context) {
action.callee = context.callee;
action.caller = context.user;
action.group = context.group;
action.server = context.server;
}
}
}
return next(action);
}

View File

@ -0,0 +1,47 @@
import { equals, ReducerRegistry } from '../base/redux';
import { SET_JWT } from './actionTypes';
/**
* The initial redux state of the feature jwt.
*
* @private
* @type {{
* isGuest: boolean
* }}
*/
const _INITIAL_STATE = {
/**
* The indicator which determines whether the local participant is a guest
* in the conference.
*
* @type {boolean}
*/
isGuest: true
};
/**
* Reduces redux actions which affect the JSON Web Token (JWT) stored in the
* redux store.
*
* @param {Object} state - The current redux state.
* @param {Object} action - The redux action to reduce.
* @returns {Object} The next redux state which is the result of reducing the
* specified {@code action}.
*/
ReducerRegistry.register('features/jwt', (state = _INITIAL_STATE, action) => {
switch (action.type) {
case SET_JWT: {
// eslint-disable-next-line no-unused-vars
const { type, ...payload } = action;
const nextState = {
..._INITIAL_STATE,
...payload
};
return equals(state, nextState) ? state : nextState;
}
}
return state;
});

View File

@ -33,6 +33,12 @@ class SecondaryToolbar extends Component {
* @static
*/
static propTypes = {
/**
* The indicator which determines whether the local participant is a
* guest in the conference.
*/
_isGuest: React.PropTypes.bool,
/**
* Handler dispatching local "Raise hand".
*/
@ -79,9 +85,14 @@ class SecondaryToolbar extends Component {
* @type {Object}
*/
profile: {
onMount: () =>
APP.tokenData.isGuest
|| this.props._onSetProfileButtonUnclickable(true)
onMount: () => {
const {
_isGuest,
_onSetProfileButtonUnclickable
} = this.props;
_isGuest || _onSetProfileButtonUnclickable(true);
}
},
/**
@ -237,18 +248,26 @@ function _mapDispatchToProps(dispatch: Function): Object {
*
* @param {Object} state - Snapshot of Redux store.
* @returns {{
* _isGuest: boolean,
* _secondaryToolbarButtons: Map,
* _visible: boolean
* }}
* @private
*/
function _mapStateToProps(state: Object): Object {
const {
secondaryToolbarButtons,
visible
} = state['features/toolbox'];
const { isGuest } = state['features/jwt'];
const { secondaryToolbarButtons, visible } = state['features/toolbox'];
return {
/**
* The indicator which determines whether the local participant is a
* guest in the conference.
*
* @private
* @type {boolean}
*/
_isGuest: isGuest,
/**
* Default toolbar buttons for secondary toolbar.
*
@ -258,7 +277,8 @@ function _mapStateToProps(state: Object): Object {
_secondaryToolbarButtons: secondaryToolbarButtons,
/**
* Shows whether toolbar is visible.
* The indicator which determines whether the {@code SecondaryToolbar}
* is visible.
*
* @private
* @type {boolean}