This commit is contained in:
30
react/features/base/jwt/actionTypes.ts
Normal file
30
react/features/base/jwt/actionTypes.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* The type of redux action which set an avatar URL for delayed loading.
|
||||
*
|
||||
* {
|
||||
* type: SET_DELAYED_LOAD_OF_AVATAR_URL
|
||||
* avatarUrl: string
|
||||
* }
|
||||
*/
|
||||
export const SET_DELAYED_LOAD_OF_AVATAR_URL = 'SET_DELAYED_LOAD_OF_AVATAR_URL';
|
||||
|
||||
/**
|
||||
* 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 = 'SET_JWT';
|
||||
|
||||
/**
|
||||
* The type of redux action which sets a known avatar URL.
|
||||
*
|
||||
* {
|
||||
* type: SET_KNOWN_AVATAR_URL,
|
||||
* avatarUrl: string
|
||||
* }
|
||||
*/
|
||||
export const SET_KNOWN_AVATAR_URL = 'SET_KNOWN_AVATAR_URL';
|
||||
49
react/features/base/jwt/actions.ts
Normal file
49
react/features/base/jwt/actions.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { SET_DELAYED_LOAD_OF_AVATAR_URL, SET_JWT, SET_KNOWN_AVATAR_URL } from './actionTypes';
|
||||
|
||||
/**
|
||||
* Sets an avatar URL for delayed loading.
|
||||
*
|
||||
* @param {string} avatarUrl - The avatar URL to set for delayed loading.
|
||||
* @returns {{
|
||||
* type: SET_DELAYED_LOAD_OF_AVATAR_URL,
|
||||
* avatarUrl: string
|
||||
* }}
|
||||
*/
|
||||
export function setDelayedLoadOfAvatarUrl(avatarUrl?: string) {
|
||||
return {
|
||||
type: SET_DELAYED_LOAD_OF_AVATAR_URL,
|
||||
avatarUrl
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores a specific JSON Web Token (JWT) into the redux store.
|
||||
*
|
||||
* @param {string} [jwt] - The JSON Web Token (JWT) to store.
|
||||
* @returns {{
|
||||
* type: SET_JWT,
|
||||
* jwt: (string|undefined)
|
||||
* }}
|
||||
*/
|
||||
export function setJWT(jwt?: string) {
|
||||
return {
|
||||
type: SET_JWT,
|
||||
jwt
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a known avatar URL.
|
||||
*
|
||||
* @param {string} avatarUrl - The avatar URL to set as known.
|
||||
* @returns {{
|
||||
* type: SET_KNOWN_AVATAR_URL,
|
||||
* avatarUrl: string
|
||||
* }}
|
||||
*/
|
||||
export function setKnownAvatarUrl(avatarUrl: string) {
|
||||
return {
|
||||
type: SET_KNOWN_AVATAR_URL,
|
||||
avatarUrl
|
||||
};
|
||||
}
|
||||
45
react/features/base/jwt/constants.ts
Normal file
45
react/features/base/jwt/constants.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { ParticipantFeaturesKey } from '../participants/types';
|
||||
|
||||
/**
|
||||
* The list of supported meeting features to enable/disable through jwt.
|
||||
*/
|
||||
export const MEET_FEATURES: Record<string, ParticipantFeaturesKey> = {
|
||||
BRANDING: 'branding',
|
||||
CALENDAR: 'calendar',
|
||||
CREATE_POLLS: 'create-polls',
|
||||
FILE_UPLOAD: 'file-upload',
|
||||
FLIP: 'flip',
|
||||
INBOUND_CALL: 'inbound-call',
|
||||
LIVESTREAMING: 'livestreaming',
|
||||
LOBBY: 'lobby',
|
||||
MODERATION: 'moderation',
|
||||
OUTBOUND_CALL: 'outbound-call',
|
||||
RECORDING: 'recording',
|
||||
ROOM: 'room',
|
||||
SCREEN_SHARING: 'screen-sharing',
|
||||
SEND_GROUPCHAT: 'send-groupchat',
|
||||
LIST_VISITORS: 'list-visitors',
|
||||
SIP_INBOUND_CALL: 'sip-inbound-call',
|
||||
SIP_OUTBOUND_CALL: 'sip-outbound-call',
|
||||
TRANSCRIPTION: 'transcription'
|
||||
};
|
||||
|
||||
/**
|
||||
* The JWT validation errors for JaaS.
|
||||
*/
|
||||
export const JWT_VALIDATION_ERRORS = {
|
||||
AUD_INVALID: 'audInvalid',
|
||||
CONTEXT_NOT_FOUND: 'contextNotFound',
|
||||
EXP_INVALID: 'expInvalid',
|
||||
FEATURE_INVALID: 'featureInvalid',
|
||||
FEATURE_VALUE_INVALID: 'featureValueInvalid',
|
||||
FEATURES_NOT_FOUND: 'featuresNotFound',
|
||||
HEADER_NOT_FOUND: 'headerNotFound',
|
||||
ISS_INVALID: 'issInvalid',
|
||||
KID_NOT_FOUND: 'kidNotFound',
|
||||
KID_MISMATCH: 'kidMismatch',
|
||||
NBF_FUTURE: 'nbfFuture',
|
||||
NBF_INVALID: 'nbfInvalid',
|
||||
PAYLOAD_NOT_FOUND: 'payloadNotFound',
|
||||
TOKEN_EXPIRED: 'tokenExpired'
|
||||
};
|
||||
241
react/features/base/jwt/functions.ts
Normal file
241
react/features/base/jwt/functions.ts
Normal file
@@ -0,0 +1,241 @@
|
||||
// @ts-expect-error
|
||||
import jwtDecode from 'jwt-decode';
|
||||
|
||||
import { IReduxState } from '../../app/types';
|
||||
import { isVpaasMeeting } from '../../jaas/functions';
|
||||
import { getLocalParticipant } from '../participants/functions';
|
||||
import { IParticipantFeatures, ParticipantFeaturesKey } from '../participants/types';
|
||||
import { parseURLParams } from '../util/parseURLParams';
|
||||
|
||||
import { JWT_VALIDATION_ERRORS, MEET_FEATURES } from './constants';
|
||||
import logger from './logger';
|
||||
|
||||
/**
|
||||
* Retrieves the JSON Web Token (JWT), if any, defined by a specific
|
||||
* {@link URL}.
|
||||
*
|
||||
* @param {URL} url - The {@code URL} to parse and retrieve the JSON Web Token
|
||||
* (JWT), if any, from.
|
||||
* @returns {string} The JSON Web Token (JWT), if any, defined by the specified
|
||||
* {@code url}; otherwise, {@code undefined}.
|
||||
*/
|
||||
export function parseJWTFromURLParams(url: URL | typeof window.location = window.location) {
|
||||
// @ts-ignore
|
||||
const jwt = parseURLParams(url, false, 'hash').jwt;
|
||||
|
||||
// TODO: eventually remove the search param and only pull from the hash
|
||||
// @ts-ignore
|
||||
return jwt ? jwt : parseURLParams(url, true, 'search').jwt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the user name after decoding the jwt.
|
||||
*
|
||||
* @param {IReduxState} state - The app state.
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getJwtName(state: IReduxState) {
|
||||
const { user } = state['features/base/jwt'];
|
||||
|
||||
return user?.name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the given JWT feature is enabled.
|
||||
*
|
||||
* @param {IReduxState} state - The app state.
|
||||
* @param {string} feature - The feature we want to check.
|
||||
* @param {boolean} ifNotInFeatures - Default value if features prop exists but does not have the {@code feature}.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isJwtFeatureEnabled(
|
||||
state: IReduxState,
|
||||
feature: ParticipantFeaturesKey,
|
||||
ifNotInFeatures: boolean
|
||||
) {
|
||||
let { features } = getLocalParticipant(state) || {};
|
||||
|
||||
if (typeof features === 'undefined' && isVpaasMeeting(state)) {
|
||||
// for vpaas the backend is always initialized with empty features if those are missing
|
||||
features = {};
|
||||
}
|
||||
|
||||
return isJwtFeatureEnabledStateless({
|
||||
localParticipantFeatures: features,
|
||||
feature,
|
||||
ifNotInFeatures
|
||||
});
|
||||
}
|
||||
|
||||
interface IIsJwtFeatureEnabledStatelessParams {
|
||||
feature: ParticipantFeaturesKey;
|
||||
ifNotInFeatures: boolean;
|
||||
jwt?: string;
|
||||
localParticipantFeatures?: IParticipantFeatures;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the given JWT feature is enabled.
|
||||
*
|
||||
* @param {ILocalParticipant} localParticipantFeatures - The features of the local participant.
|
||||
* @param {string} feature - The feature we want to check.
|
||||
* @param {boolean} ifNotInFeatures - Default value if features is missing
|
||||
* or prop exists but does not have the {@code feature}.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isJwtFeatureEnabledStateless({
|
||||
localParticipantFeatures: features,
|
||||
feature,
|
||||
ifNotInFeatures
|
||||
}: IIsJwtFeatureEnabledStatelessParams) {
|
||||
if (typeof features?.[feature] === 'undefined') {
|
||||
return ifNotInFeatures;
|
||||
}
|
||||
|
||||
return String(features[feature]) === 'true';
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a given timestamp is a valid UNIX timestamp in seconds.
|
||||
* We convert to milliseconds during the check since `Date` works with milliseconds for UNIX timestamp values.
|
||||
*
|
||||
* @param {any} timestamp - A UNIX timestamp in seconds as stored in the jwt.
|
||||
* @returns {boolean} - Whether the timestamp is indeed a valid UNIX timestamp or not.
|
||||
*/
|
||||
function isValidUnixTimestamp(timestamp: number | string) {
|
||||
return typeof timestamp === 'number' && timestamp * 1000 === new Date(timestamp * 1000).getTime();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list with all validation errors for the given jwt.
|
||||
*
|
||||
* @param {string} jwt - The jwt.
|
||||
* @returns {Array} - An array containing all jwt validation errors.
|
||||
*/
|
||||
export function validateJwt(jwt: string) {
|
||||
const errors: Object[] = [];
|
||||
const currentTimestamp = new Date().getTime();
|
||||
|
||||
try {
|
||||
const header = jwtDecode(jwt, { header: true });
|
||||
const payload = jwtDecode(jwt);
|
||||
|
||||
if (!header) {
|
||||
errors.push({ key: JWT_VALIDATION_ERRORS.HEADER_NOT_FOUND });
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
if (!payload) {
|
||||
errors.push({ key: JWT_VALIDATION_ERRORS.PAYLOAD_NOT_FOUND });
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
const {
|
||||
aud,
|
||||
context,
|
||||
exp,
|
||||
iss,
|
||||
nbf,
|
||||
sub
|
||||
} = payload;
|
||||
|
||||
// JaaS only
|
||||
if (sub?.startsWith('vpaas-magic-cookie')) {
|
||||
const { kid } = header;
|
||||
|
||||
// if Key ID is missing, we return the error immediately without further validations.
|
||||
if (!kid) {
|
||||
errors.push({ key: JWT_VALIDATION_ERRORS.KID_NOT_FOUND });
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
if (kid.substring(0, kid.indexOf('/')) !== sub) {
|
||||
errors.push({ key: JWT_VALIDATION_ERRORS.KID_MISMATCH });
|
||||
}
|
||||
|
||||
if (aud !== 'jitsi') {
|
||||
errors.push({ key: JWT_VALIDATION_ERRORS.AUD_INVALID });
|
||||
}
|
||||
|
||||
if (iss !== 'chat') {
|
||||
errors.push({ key: JWT_VALIDATION_ERRORS.ISS_INVALID });
|
||||
}
|
||||
|
||||
if (!context?.features) {
|
||||
errors.push({ key: JWT_VALIDATION_ERRORS.FEATURES_NOT_FOUND });
|
||||
}
|
||||
}
|
||||
|
||||
if (nbf) { // nbf value is optional
|
||||
if (!isValidUnixTimestamp(nbf)) {
|
||||
errors.push({ key: JWT_VALIDATION_ERRORS.NBF_INVALID });
|
||||
} else if (currentTimestamp < nbf * 1000) {
|
||||
errors.push({ key: JWT_VALIDATION_ERRORS.NBF_FUTURE });
|
||||
}
|
||||
}
|
||||
|
||||
if (!isValidUnixTimestamp(exp)) {
|
||||
errors.push({ key: JWT_VALIDATION_ERRORS.EXP_INVALID });
|
||||
} else if (currentTimestamp > exp * 1000) {
|
||||
errors.push({ key: JWT_VALIDATION_ERRORS.TOKEN_EXPIRED });
|
||||
}
|
||||
|
||||
if (!context) {
|
||||
errors.push({ key: JWT_VALIDATION_ERRORS.CONTEXT_NOT_FOUND });
|
||||
} else if (context.features) {
|
||||
const { features } = context;
|
||||
const meetFeatures = Object.values(MEET_FEATURES);
|
||||
|
||||
(Object.keys(features) as ParticipantFeaturesKey[]).forEach(feature => {
|
||||
if (meetFeatures.includes(feature)) {
|
||||
const featureValue = features[feature];
|
||||
|
||||
// cannot use truthy or falsy because we need the exact value and type check.
|
||||
if (
|
||||
featureValue !== true
|
||||
&& featureValue !== false
|
||||
&& featureValue !== 'true'
|
||||
&& featureValue !== 'false'
|
||||
) {
|
||||
errors.push({
|
||||
key: JWT_VALIDATION_ERRORS.FEATURE_VALUE_INVALID,
|
||||
args: { feature }
|
||||
});
|
||||
}
|
||||
} else {
|
||||
errors.push({
|
||||
key: JWT_VALIDATION_ERRORS.FEATURE_INVALID,
|
||||
args: { feature }
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (e: any) {
|
||||
logger.error(`Unspecified JWT error${e?.message ? `: ${e.message}` : ''}`);
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts and returns the expiration date of jwt.
|
||||
*
|
||||
* @param {string|undefined} jwt - The jwt to check.
|
||||
* @returns {Date} The expiration date of the jwt.
|
||||
*/
|
||||
export function getJwtExpirationDate(jwt: string | undefined) {
|
||||
if (!jwt) {
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = jwtDecode(jwt);
|
||||
|
||||
if (payload) {
|
||||
const { exp } = payload;
|
||||
|
||||
return new Date(exp * 1000);
|
||||
}
|
||||
}
|
||||
3
react/features/base/jwt/logger.ts
Normal file
3
react/features/base/jwt/logger.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { getLogger } from '../logging/functions';
|
||||
|
||||
export default getLogger('features/base/jwt');
|
||||
323
react/features/base/jwt/middleware.ts
Normal file
323
react/features/base/jwt/middleware.ts
Normal file
@@ -0,0 +1,323 @@
|
||||
// @ts-expect-error
|
||||
import jwtDecode from 'jwt-decode';
|
||||
import { AnyAction } from 'redux';
|
||||
|
||||
import { IStore } from '../../app/types';
|
||||
import { isVpaasMeeting } from '../../jaas/functions';
|
||||
import { getCurrentConference } from '../conference/functions';
|
||||
import { SET_CONFIG } from '../config/actionTypes';
|
||||
import { CONNECTION_ESTABLISHED, SET_LOCATION_URL } from '../connection/actionTypes';
|
||||
import { participantUpdated } from '../participants/actions';
|
||||
import { getLocalParticipant } from '../participants/functions';
|
||||
import { IParticipant } from '../participants/types';
|
||||
import MiddlewareRegistry from '../redux/MiddlewareRegistry';
|
||||
import StateListenerRegistry from '../redux/StateListenerRegistry';
|
||||
import { parseURIString } from '../util/uri';
|
||||
|
||||
import { SET_JWT } from './actionTypes';
|
||||
import { setDelayedLoadOfAvatarUrl, setJWT, setKnownAvatarUrl } from './actions';
|
||||
import { parseJWTFromURLParams } from './functions';
|
||||
import logger from './logger';
|
||||
|
||||
/**
|
||||
* Set up a state change listener to perform maintenance tasks when the conference
|
||||
* is left or failed, e.g. Clear any delayed load avatar url.
|
||||
*/
|
||||
StateListenerRegistry.register(
|
||||
state => getCurrentConference(state),
|
||||
(conference, { dispatch }, previousConference): void => {
|
||||
if (conference !== previousConference) {
|
||||
dispatch(setDelayedLoadOfAvatarUrl());
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 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_LOCATION_URL:
|
||||
// XXX The JSON Web Token (JWT) is not the only piece of state that we
|
||||
// have decided to store in the feature jwt
|
||||
return _setConfigOrLocationURL(store, next, action);
|
||||
case CONNECTION_ESTABLISHED: {
|
||||
const state = store.getState();
|
||||
const delayedLoadOfAvatarUrl = state['features/base/jwt'].delayedLoadOfAvatarUrl;
|
||||
|
||||
if (delayedLoadOfAvatarUrl) {
|
||||
_overwriteLocalParticipant(store, {
|
||||
avatarURL: delayedLoadOfAvatarUrl
|
||||
});
|
||||
store.dispatch(setDelayedLoadOfAvatarUrl());
|
||||
store.dispatch(setKnownAvatarUrl(delayedLoadOfAvatarUrl));
|
||||
}
|
||||
}
|
||||
case SET_JWT:
|
||||
return _setJWT(store, next, action);
|
||||
}
|
||||
|
||||
return next(action);
|
||||
});
|
||||
|
||||
/**
|
||||
* Overwrites the properties {@code avatarURL}, {@code email}, and {@code name}
|
||||
* of the local participant stored in the redux state base/participants.
|
||||
*
|
||||
* @param {Store} store - The redux store.
|
||||
* @param {Object} localParticipant - The {@code Participant} structure to
|
||||
* overwrite the local participant stored in the redux store base/participants
|
||||
* with.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
function _overwriteLocalParticipant(
|
||||
{ dispatch, getState }: IStore,
|
||||
{ avatarURL, email, id: jwtId, name, features }:
|
||||
{ avatarURL?: string; email?: string; features?: any; id?: string; name?: string; }) {
|
||||
let localParticipant;
|
||||
|
||||
if ((avatarURL || email || name || features) && (localParticipant = getLocalParticipant(getState))) {
|
||||
const newProperties: IParticipant = {
|
||||
id: localParticipant.id,
|
||||
local: true
|
||||
};
|
||||
|
||||
if (avatarURL) {
|
||||
newProperties.avatarURL = avatarURL;
|
||||
}
|
||||
if (email) {
|
||||
newProperties.email = email;
|
||||
}
|
||||
if (jwtId) {
|
||||
newProperties.jwtId = jwtId;
|
||||
}
|
||||
if (name) {
|
||||
newProperties.name = name;
|
||||
}
|
||||
if (features) {
|
||||
newProperties.features = features;
|
||||
}
|
||||
dispatch(participantUpdated(newProperties));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies the feature jwt that the action {@link SET_CONFIG} or
|
||||
* {@link SET_LOCATION_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_LOCATION_URL} 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 _setConfigOrLocationURL({ dispatch, getState }: IStore, next: Function, action: AnyAction) {
|
||||
const result = next(action);
|
||||
|
||||
const { locationURL } = getState()['features/base/connection'];
|
||||
|
||||
dispatch(
|
||||
setJWT(locationURL ? parseJWTFromURLParams(locationURL) : undefined));
|
||||
|
||||
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(store: IStore, next: Function, action: AnyAction) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { jwt, type, ...actionPayload } = action;
|
||||
|
||||
if (!Object.keys(actionPayload).length) {
|
||||
const state = store.getState();
|
||||
|
||||
if (jwt) {
|
||||
let jwtPayload;
|
||||
|
||||
try {
|
||||
jwtPayload = jwtDecode(jwt);
|
||||
} catch (e) {
|
||||
logger.error(e);
|
||||
}
|
||||
|
||||
if (jwtPayload) {
|
||||
const { context, iss, sub } = jwtPayload;
|
||||
const { tokenGetUserInfoOutOfContext, tokenRespectTenant } = state['features/base/config'];
|
||||
|
||||
action.jwt = jwt;
|
||||
action.issuer = iss;
|
||||
if (context) {
|
||||
const user = _user2participant(context.user || {});
|
||||
|
||||
action.callee = context.callee;
|
||||
action.group = context.group;
|
||||
action.server = context.server;
|
||||
action.tenant = context.tenant || sub || undefined;
|
||||
action.user = user;
|
||||
|
||||
const newUser = user ? { ...user } : {};
|
||||
|
||||
let features = context.features;
|
||||
|
||||
// eslint-disable-next-line max-depth
|
||||
if (!isVpaasMeeting(state) && tokenRespectTenant && context.tenant) {
|
||||
// we skip checking vpaas meetings as there are other backend rules in place
|
||||
// this way vpaas users can still use this field if needed
|
||||
const { locationURL = { href: '' } as URL } = state['features/base/connection'];
|
||||
const { tenant = '' } = parseURIString(locationURL.href) || {};
|
||||
|
||||
features = context.tenant === tenant || tenant === '' ? features : {};
|
||||
}
|
||||
|
||||
if (newUser.avatarURL) {
|
||||
const { knownAvatarUrl } = state['features/base/jwt'];
|
||||
|
||||
if (knownAvatarUrl !== newUser.avatarURL) {
|
||||
store.dispatch(setDelayedLoadOfAvatarUrl(newUser.avatarURL));
|
||||
|
||||
newUser.avatarURL = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
_overwriteLocalParticipant(
|
||||
store, { ...newUser,
|
||||
features });
|
||||
|
||||
// eslint-disable-next-line max-depth
|
||||
if (context.user && context.user.role === 'visitor') {
|
||||
action.preferVisitor = true;
|
||||
}
|
||||
} else if (tokenGetUserInfoOutOfContext
|
||||
&& (jwtPayload.name || jwtPayload.picture || jwtPayload.email)) {
|
||||
// there are some tokens (firebase) having picture and name on the main level.
|
||||
_overwriteLocalParticipant(store, {
|
||||
avatarURL: jwtPayload.picture,
|
||||
name: jwtPayload.name,
|
||||
email: jwtPayload.email
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (typeof APP === 'undefined') {
|
||||
// The logic of restoring JWT overrides make sense only on mobile.
|
||||
// On Web it should eventually be restored from storage, but there's
|
||||
// no such use case yet.
|
||||
|
||||
const { user } = state['features/base/jwt'];
|
||||
|
||||
user && _undoOverwriteLocalParticipant(store, user);
|
||||
}
|
||||
}
|
||||
|
||||
return next(action);
|
||||
}
|
||||
|
||||
/**
|
||||
* Undoes/resets the values overwritten by {@link _overwriteLocalParticipant}
|
||||
* by either clearing them or setting to default values. Only the values that
|
||||
* have not changed since the overwrite happened will be restored.
|
||||
*
|
||||
* NOTE Once it is possible to edit and save participant properties, this
|
||||
* function should restore values from the storage instead.
|
||||
*
|
||||
* @param {Store} store - The redux store.
|
||||
* @param {Object} localParticipant - The {@code Participant} structure used
|
||||
* previously to {@link _overwriteLocalParticipant}.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
function _undoOverwriteLocalParticipant(
|
||||
{ dispatch, getState }: IStore,
|
||||
{ avatarURL, name, email }: { avatarURL?: string; email?: string; name?: string; }) {
|
||||
let localParticipant;
|
||||
|
||||
if ((avatarURL || name || email)
|
||||
&& (localParticipant = getLocalParticipant(getState))) {
|
||||
const newProperties: IParticipant = {
|
||||
id: localParticipant.id,
|
||||
local: true
|
||||
};
|
||||
|
||||
if (avatarURL === localParticipant.avatarURL) {
|
||||
newProperties.avatarURL = undefined;
|
||||
}
|
||||
if (email === localParticipant.email) {
|
||||
newProperties.email = undefined;
|
||||
}
|
||||
if (name === localParticipant.name) {
|
||||
newProperties.name = undefined;
|
||||
}
|
||||
newProperties.features = undefined;
|
||||
|
||||
dispatch(participantUpdated(newProperties));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the JWT {@code context.user} structure to the {@code Participant}
|
||||
* structure stored in the redux state base/participants.
|
||||
*
|
||||
* @param {Object} user - The JWT {@code context.user} structure to convert.
|
||||
* @private
|
||||
* @returns {{
|
||||
* avatarURL: ?string,
|
||||
* email: ?string,
|
||||
* id: ?string,
|
||||
* name: ?string,
|
||||
* hidden-from-recorder: ?boolean
|
||||
* }}
|
||||
*/
|
||||
function _user2participant({ avatar, avatarUrl, email, id, name, 'hidden-from-recorder': hiddenFromRecorder }:
|
||||
{ avatar?: string; avatarUrl?: string; email: string; 'hidden-from-recorder': string | boolean;
|
||||
id: string; name: string; }) {
|
||||
const participant: {
|
||||
avatarURL?: string;
|
||||
email?: string;
|
||||
hiddenFromRecorder?: boolean;
|
||||
id?: string;
|
||||
name?: string;
|
||||
} = {};
|
||||
|
||||
if (typeof avatarUrl === 'string') {
|
||||
participant.avatarURL = avatarUrl.trim();
|
||||
} else if (typeof avatar === 'string') {
|
||||
participant.avatarURL = avatar.trim();
|
||||
}
|
||||
if (typeof email === 'string') {
|
||||
participant.email = email.trim();
|
||||
}
|
||||
if (typeof id === 'string') {
|
||||
participant.id = id.trim();
|
||||
}
|
||||
if (typeof name === 'string') {
|
||||
participant.name = name.trim();
|
||||
}
|
||||
|
||||
if (hiddenFromRecorder === 'true' || hiddenFromRecorder === true) {
|
||||
participant.hiddenFromRecorder = true;
|
||||
}
|
||||
|
||||
return Object.keys(participant).length ? participant : undefined;
|
||||
}
|
||||
73
react/features/base/jwt/reducer.ts
Normal file
73
react/features/base/jwt/reducer.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import PersistenceRegistry from '../redux/PersistenceRegistry';
|
||||
import ReducerRegistry from '../redux/ReducerRegistry';
|
||||
import { equals } from '../redux/functions';
|
||||
|
||||
import { SET_DELAYED_LOAD_OF_AVATAR_URL, SET_JWT, SET_KNOWN_AVATAR_URL } from './actionTypes';
|
||||
import logger from './logger';
|
||||
|
||||
export interface IJwtState {
|
||||
callee?: {
|
||||
name: string;
|
||||
};
|
||||
delayedLoadOfAvatarUrl?: string;
|
||||
group?: string;
|
||||
jwt?: string;
|
||||
knownAvatarUrl?: string;
|
||||
server?: string;
|
||||
tenant?: string;
|
||||
user?: {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
}
|
||||
|
||||
PersistenceRegistry.register('features/base/jwt', {
|
||||
knownAvatarUrl: 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<IJwtState>(
|
||||
'features/base/jwt',
|
||||
(state = {}, action): IJwtState => {
|
||||
switch (action.type) {
|
||||
case SET_DELAYED_LOAD_OF_AVATAR_URL: {
|
||||
const nextState = {
|
||||
...state,
|
||||
delayedLoadOfAvatarUrl: action.avatarUrl
|
||||
};
|
||||
|
||||
if (equals(state, nextState)) {
|
||||
return state;
|
||||
}
|
||||
|
||||
logger.info('JWT avatarURL temporarily not loaded till jwt is verified on connect');
|
||||
|
||||
return nextState;
|
||||
}
|
||||
case SET_JWT: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { type, ...payload } = action;
|
||||
const nextState = {
|
||||
...state,
|
||||
...payload
|
||||
};
|
||||
|
||||
return equals(state, nextState) ? state : nextState;
|
||||
}
|
||||
case SET_KNOWN_AVATAR_URL:
|
||||
return {
|
||||
...state,
|
||||
knownAvatarUrl: action.avatarUrl
|
||||
};
|
||||
}
|
||||
|
||||
return state;
|
||||
});
|
||||
Reference in New Issue
Block a user