From dca40dc6cb04ec6cc05b05b3cfba3efa9ede8d23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sa=C3=BAl=20Ibarra=20Corretg=C3=A9?= Date: Tue, 15 Aug 2023 11:46:45 +0200 Subject: [PATCH] feat(rn,auth) add support for toekn URL auth --- .../features/authentication/actions.native.ts | 40 ++++++++++- react/features/authentication/actions.web.ts | 33 ++++++++- .../authentication/components/index.native.ts | 1 - .../authentication/components/index.web.ts | 1 - .../components/native/WaitForOwnerDialog.tsx | 7 +- react/features/authentication/functions.ts | 2 +- react/features/authentication/middleware.ts | 67 +++++++------------ react/features/base/util/uri.ts | 26 +++++++ .../features/mobile/navigation/functions.tsx | 6 +- 9 files changed, 130 insertions(+), 53 deletions(-) diff --git a/react/features/authentication/actions.native.ts b/react/features/authentication/actions.native.ts index da07baf63f..ac2e469267 100644 --- a/react/features/authentication/actions.native.ts +++ b/react/features/authentication/actions.native.ts @@ -1,10 +1,13 @@ +import { Linking, Platform } from 'react-native'; + import { appNavigate } from '../app/actions.native'; import { IStore } from '../app/types'; import { conferenceLeft } from '../base/conference/actions'; import { connectionFailed } from '../base/connection/actions.native'; import { set } from '../base/redux/functions'; +import { appendURLHashParam } from '../base/util/uri'; -import { CANCEL_LOGIN } from './actionTypes'; +import { CANCEL_LOGIN, LOGIN } from './actionTypes'; import { stopWaitForOwner } from './actions.any'; export * from './actions.any'; @@ -51,7 +54,7 @@ export function cancelWaitForOwner() { // recoverable by the feature room-lock and, consequently, // recoverable-aware features such as mobile's external-api did not // deliver the CONFERENCE_FAILED to the SDK clients/consumers. Since the - // app/user is going to nativate to WelcomePage, the SDK + // app/user is going to navigate to WelcomePage, the SDK // clients/consumers need an event. const { authRequired } = getState()['features/base/conference']; @@ -65,7 +68,7 @@ export function cancelWaitForOwner() { }; } -/** . +/** * Redirect to the default location (e.g. Welcome page). * * @returns {Function} @@ -73,3 +76,34 @@ export function cancelWaitForOwner() { export function redirectToDefaultLocation() { return (dispatch: IStore['dispatch']) => dispatch(appNavigate(undefined)); } + +/** + * Opens token auth URL page. + * + * @param {string} tokenAuthServiceUrl - Authentication service URL. + * + * @returns {Function} + */ +export function openTokenAuthUrl(tokenAuthServiceUrl: string) { + let url = appendURLHashParam(tokenAuthServiceUrl, 'skipPrejoin', 'true'); + + // Append ios=true or android=true to the token URL. + url = appendURLHashParam(url, Platform.OS, 'true'); + + return () => { + Linking.openURL(url); + }; +} + +/** + * Login. + * + * @returns {{ + * type: LOGIN + * }} + */ +export function login() { + return { + type: LOGIN + }; +} diff --git a/react/features/authentication/actions.web.ts b/react/features/authentication/actions.web.ts index 86131043dd..d373a053de 100644 --- a/react/features/authentication/actions.web.ts +++ b/react/features/authentication/actions.web.ts @@ -1,11 +1,14 @@ import { maybeRedirectToWelcomePage } from '../app/actions.web'; import { IStore } from '../app/types'; +import { openDialog } from '../base/dialog/actions'; +import { appendURLHashParam } from '../base/util/uri'; import { CANCEL_LOGIN, LOGIN, LOGOUT } from './actionTypes'; +import LoginQuestionDialog from './components/web/LoginQuestionDialog'; export * from './actions.any'; @@ -38,7 +41,7 @@ export function cancelWaitForOwner() { }; } -/** . +/** * Redirect to the default location (e.g. Welcome page). * * @returns {Function} @@ -72,3 +75,31 @@ export function logout() { type: LOGOUT }; } + +/** + * Opens token auth URL page. + * + * @param {string} tokenAuthServiceUrl - Authentication service URL. + * + * @returns {Function} + */ +export function openTokenAuthUrl(tokenAuthServiceUrl: string): any { + return (dispatch: IStore['dispatch'], getState: IStore['getState']) => { + const redirect = () => { + // We have already shown the prejoin screen, no need to show it again after obtaining the token. + window.location.href = appendURLHashParam(tokenAuthServiceUrl, 'skipPrejoin', 'true'); + }; + + // Show warning for leaving conference only when in a conference. + if (getState()['features/base/conference'].conference) { + dispatch(openDialog(LoginQuestionDialog, { + handler: () => { + // Give time for the dialog to close. + setTimeout(() => redirect, 500); + } + })); + } else { + redirect(); + } + }; +} diff --git a/react/features/authentication/components/index.native.ts b/react/features/authentication/components/index.native.ts index fb82e85fb9..12ba899149 100644 --- a/react/features/authentication/components/index.native.ts +++ b/react/features/authentication/components/index.native.ts @@ -1,3 +1,2 @@ export { default as LoginDialog } from './native/LoginDialog'; -export const LoginQuestionDialog = undefined; export { default as WaitForOwnerDialog } from './native/WaitForOwnerDialog'; diff --git a/react/features/authentication/components/index.web.ts b/react/features/authentication/components/index.web.ts index 97f7606533..3f648f2342 100644 --- a/react/features/authentication/components/index.web.ts +++ b/react/features/authentication/components/index.web.ts @@ -1,3 +1,2 @@ export { default as LoginDialog } from './web/LoginDialog'; -export { default as LoginQuestionDialog } from './web/LoginQuestionDialog'; export { default as WaitForOwnerDialog } from './web/WaitForOwnerDialog'; diff --git a/react/features/authentication/components/native/WaitForOwnerDialog.tsx b/react/features/authentication/components/native/WaitForOwnerDialog.tsx index 8f232ccc8a..46d02608f5 100644 --- a/react/features/authentication/components/native/WaitForOwnerDialog.tsx +++ b/react/features/authentication/components/native/WaitForOwnerDialog.tsx @@ -4,7 +4,8 @@ import { connect } from 'react-redux'; import { IStore } from '../../../app/types'; import ConfirmDialog from '../../../base/dialog/components/native/ConfirmDialog'; import { translate } from '../../../base/i18n/functions'; -import { cancelWaitForOwner, openLoginDialog } from '../../actions.native'; +import { cancelWaitForOwner, login } from '../../actions.native'; + /** * The type of the React {@code Component} props of {@link WaitForOwnerDialog}. @@ -77,7 +78,9 @@ class WaitForOwnerDialog extends Component { * @returns {void} */ _onLogin() { - this.props.dispatch(openLoginDialog()); + this.props.dispatch(login()); + + return true; } } diff --git a/react/features/authentication/functions.ts b/react/features/authentication/functions.ts index 9657338036..cc9bf15180 100644 --- a/react/features/authentication/functions.ts +++ b/react/features/authentication/functions.ts @@ -27,7 +27,7 @@ export const isTokenAuthEnabled = (config: IConfig) => * undefined if the pattern stored in config is not a string and the URL can not be * constructed. */ -export const getTokenAuthUrl = (config: IConfig, roomName: string) => { +export const getTokenAuthUrl = (config: IConfig, roomName: string | undefined) => { const url = config.tokenAuthUrl; diff --git a/react/features/authentication/middleware.ts b/react/features/authentication/middleware.ts index 7538b834b6..125c2f363c 100644 --- a/react/features/authentication/middleware.ts +++ b/react/features/authentication/middleware.ts @@ -8,7 +8,7 @@ import { import { isRoomValid } from '../base/conference/functions'; import { CONNECTION_ESTABLISHED, CONNECTION_FAILED } from '../base/connection/actionTypes'; import { hangup } from '../base/connection/actions'; -import { hideDialog, openDialog } from '../base/dialog/actions'; +import { hideDialog } from '../base/dialog/actions'; import { isDialogOpen } from '../base/dialog/functions'; import { JitsiConferenceErrors, @@ -16,8 +16,6 @@ import { } from '../base/lib-jitsi-meet'; import MiddlewareRegistry from '../base/redux/MiddlewareRegistry'; import { getBackendSafeRoomName } from '../base/util/uri'; -import { showErrorNotification } from '../notifications/actions'; -import { NOTIFICATION_TIMEOUT_TYPE } from '../notifications/constants'; import { openLogoutDialog } from '../settings/actions'; import { @@ -31,14 +29,16 @@ import { import { hideLoginDialog, openLoginDialog, + openTokenAuthUrl, openWaitForOwnerDialog, redirectToDefaultLocation, setTokenAuthUrlSuccess, stopWaitForOwner, waitForOwner } from './actions'; -import { LoginDialog, LoginQuestionDialog, WaitForOwnerDialog } from './components'; +import { LoginDialog, WaitForOwnerDialog } from './components'; import { getTokenAuthUrl, isTokenAuthEnabled } from './functions'; +import logger from './logger'; /** * Middleware that captures connection or conference failed errors and controls @@ -282,45 +282,26 @@ function _handleLogin({ dispatch, getState }: IStore) { const config = state['features/base/config']; const room = getBackendSafeRoomName(state['features/base/conference'].room); - if (isTokenAuthEnabled(config)) { - if (typeof APP === 'undefined') { - dispatch(showErrorNotification({ - descriptionKey: 'dialog.tokenAuthUnsupported', - titleKey: 'dialog.tokenAuthFailedTitle' - }, NOTIFICATION_TIMEOUT_TYPE.LONG)); + if (!room) { + logger.warn('Cannot handle login, room is undefined!'); - dispatch(redirectToDefaultLocation()); - - return; - } - - if (!room) { - return; - } - - // FIXME: This method will not preserve the other URL params that were originally passed. - const tokenAuthServiceUrl = getTokenAuthUrl(config, room); - - if (tokenAuthServiceUrl && LoginQuestionDialog) { - const redirect = () => { - // we have already shown the prejoin screen, no need to show it again after obtaining the token - window.location.href = `${tokenAuthServiceUrl}${ - tokenAuthServiceUrl.includes('#') ? '&' : '#'}skipPrejoin=true`; - }; - - // show warning for leaving conference only when in a conference - if (state['features/base/conference'].conference) { - dispatch(openDialog(LoginQuestionDialog, { - handler: () => { - // give time for the dialog to close - setTimeout(redirect, 500); - } - })); - } else { - redirect(); - } - } - } else { - dispatch(openLoginDialog()); + return; } + + if (!isTokenAuthEnabled(config)) { + dispatch(openLoginDialog()); + + return; + } + + // FIXME: This method will not preserve the other URL params that were originally passed. + const tokenAuthServiceUrl = getTokenAuthUrl(config, room); + + if (!tokenAuthServiceUrl) { + logger.warn('Cannot handle login, token service URL is not set'); + + return; + } + + dispatch(openTokenAuthUrl(tokenAuthServiceUrl)); } diff --git a/react/features/base/util/uri.ts b/react/features/base/util/uri.ts index 6d30c31f1c..29ddf3a90e 100644 --- a/react/features/base/util/uri.ts +++ b/react/features/base/util/uri.ts @@ -631,3 +631,29 @@ export function appendURLParam(url: string, name: string, value: string) { return newUrl.toString(); } + +/** + * Adds new hash param to a url string. + * Checks whether to use '?' or '&' as a separator (checks for already existing params). + * + * @param {string} url - The url to modify. + * @param {string} name - The param name to add. + * @param {string} value - The value for the param. + * + * @returns {string} - The modified url. + */ +export function appendURLHashParam(url: string, name: string, value: string) { + const newUrl = new URL(url); + const dummyUrl = new URL('https://example.com'); + + // Copy current hash-parameters without the '#' as search-parameters. + dummyUrl.search = newUrl.hash.substring(1); + + // Set or update value with the searchParams-API. + dummyUrl.searchParams.append(name, value); + + // Write back as hash parameters. + newUrl.hash = dummyUrl.searchParams.toString(); + + return newUrl.toString(); +} diff --git a/react/features/mobile/navigation/functions.tsx b/react/features/mobile/navigation/functions.tsx index ba99babd13..fe9e0efb64 100644 --- a/react/features/mobile/navigation/functions.tsx +++ b/react/features/mobile/navigation/functions.tsx @@ -49,7 +49,11 @@ export function screenHeaderCloseButton(goBack: (e?: GestureResponderEvent | Rea * {@code true}; otherwise, {@code false}. */ export function isPrejoinPageEnabled(stateful: IStateful) { - return getFeatureFlag(toState(stateful), PREJOIN_PAGE_ENABLED, true); + const state = toState(stateful); + const { prejoinConfig } = state['features/base/config']; + const isPrejoinEnabledInConfig = prejoinConfig?.enabled; + + return getFeatureFlag(state, PREJOIN_PAGE_ENABLED, isPrejoinEnabledInConfig ?? true); } /**