From b0bc84543d2cc63470bfe067c4384e6e09e7cd25 Mon Sep 17 00:00:00 2001 From: tamarafinogina Date: Thu, 16 Apr 2026 18:15:48 +0200 Subject: [PATCH 01/11] add leaveCall on page hiding action --- .gitignore | 1 + react/features/base/connection/actions.any.ts | 5 ++- react/features/base/connection/actions.web.ts | 15 +++++--- .../base/meet/views/Conference/Conference.tsx | 38 ++++++++++++++++++- 4 files changed, 50 insertions(+), 9 deletions(-) diff --git a/.gitignore b/.gitignore index 747e9fd0e232..5aa01f82a319 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ all.css .remote-sync.json .sync-config.cson .env +.env.* .npmrc # Coverage diff --git a/react/features/base/connection/actions.any.ts b/react/features/base/connection/actions.any.ts index 9f3c63a68792..3641fa6f2bef 100644 --- a/react/features/base/connection/actions.any.ts +++ b/react/features/base/connection/actions.any.ts @@ -31,6 +31,7 @@ import { JITSI_CONNECTION_URL_KEY } from "./constants"; import logger from "./logger"; import { get8x8Options } from "./options8x8"; import { ConnectionFailedError, IIceServers } from "./types"; +import { ConfigService } from '../meet/services/config.service'; /** * The options that will be passed to the JitsiConnection instance. @@ -155,7 +156,9 @@ export function constructOptions(state: IReduxState) { options.websocketKeepAliveUrl = appendURLParam(options.websocketKeepAliveUrl, "room", roomName ?? ""); } if (options.conferenceRequestUrl) { - options.conferenceRequestUrl = appendURLParam(options.conferenceRequestUrl, "room", roomName ?? ""); + options.conferenceRequestUrl = ConfigService.instance.isDevelopment() + ? undefined + : appendURLParam(options.conferenceRequestUrl, "room", roomName ?? ""); } } diff --git a/react/features/base/connection/actions.web.ts b/react/features/base/connection/actions.web.ts index f36349c58d82..eba0caff7d13 100644 --- a/react/features/base/connection/actions.web.ts +++ b/react/features/base/connection/actions.web.ts @@ -17,13 +17,17 @@ import logger from './logger'; /** * Helper function to leave a call with proper user identification (authenticated or anonymous) + * If authenticated user - uuid is taken from the token, else - sent anonymous uuid from local storage * @param {string} roomId - The room ID to leave * @returns {Promise} */ async function leaveCallWithUserIdentification(roomId: string): Promise { const user = LocalStorageManager.instance.getUser(); - const anonymousUserId = user ? undefined : LocalStorageManager.instance.getAnonymousUUID(); - return await MeetingService.instance.leaveCall(roomId, anonymousUserId ? { userId: anonymousUserId } : undefined); + let payload = undefined; + if (!user){ + payload = { userId: LocalStorageManager.instance.getAnonymousUUID() || '' }; + } + return await MeetingService.instance.leaveCall(roomId, payload); } export * from "./actions.any"; @@ -139,14 +143,13 @@ export function hangup(requestFeedback = false, roomId?: string, feedbackTitle?: export async function cleanupAndReload(roomId: string) { try{ - console.log('[RELOAD_PAGE]: Leaving the call'); + console.log('[RELOAD] cleanupAndReload is called:', roomId); await leaveCallWithUserIdentification(roomId); - console.log('[RELOAD_PAGE]: Cleaning up the conference'); await APP.conference.cleanup(); } catch (error) { - console.error("[RELOAD_PAGE]: Error during cleanup and reload", error); + console.error("[RELOAD]: Error during cleanup and reload", error); } finally { - console.log("[RELOAD_PAGE]: Reloading the page"); + console.log("[RELOAD]: Reloading the page"); window.location.reload(); } } diff --git a/react/features/base/meet/views/Conference/Conference.tsx b/react/features/base/meet/views/Conference/Conference.tsx index 0d75173f9a7c..4b5c09558f30 100644 --- a/react/features/base/meet/views/Conference/Conference.tsx +++ b/react/features/base/meet/views/Conference/Conference.tsx @@ -20,6 +20,8 @@ import { init } from "../../../../conference/actions.web"; import CreateConference from "./containers/CreateConference"; import JoinConference from "./containers/JoinConference"; import { appNavigate } from "../../../../app/actions.web"; +import { LocalStorageManager } from "../../LocalStorageManager"; +import { ConfigService } from "../../services/config.service"; /** * DOM events for when full screen mode has changed. Different browsers need @@ -165,6 +167,7 @@ class Conference extends AbstractConference { window.addEventListener("beforeunload", this._handleBeforeUnload, true); window.addEventListener("popstate", this._handlePopState); + window.addEventListener("pagehide", this._handlePageHide, true); } /** @@ -193,14 +196,18 @@ class Conference extends AbstractConference { * @inheritdoc */ override componentWillUnmount() { + console.log('[RELOAD] Conference component will unmount, leaving the call and cleaning up'); APP.UI.unbindEvents(); FULL_SCREEN_EVENTS.forEach((name) => document.removeEventListener(name, this._onFullScreenChange)); window.removeEventListener("beforeunload", this._handleBeforeUnload, true); window.removeEventListener("popstate", this._handlePopState); + window.removeEventListener("pagehide", this._handlePageHide, true); - APP.conference.isJoined() && this.props.dispatch(hangup(true, this.props.roomId)); + if (APP.conference.isJoined()) { + this.props.dispatch(hangup(true, this.props.roomId)); + } } /** @@ -216,12 +223,38 @@ class Conference extends AbstractConference { event.preventDefault(); event.stopImmediatePropagation(); - event.returnValue = ''; return ''; } return ""; }; + _handlePageHide = (): void => { + console.log("[RELOAD]: Page is being hidden, sending leave call request"); + + const callId = this.props.roomId; + const token = LocalStorageManager.instance.getNewToken(); + let body = ''; + if(!token) { + const anonymousUserId = LocalStorageManager.instance.getAnonymousUUID(); + body = JSON.stringify({ userId: anonymousUserId }); + } + + const MEET_API_URL = ConfigService.instance.get("MEET_API_URL"); + + fetch(`${MEET_API_URL}/call/${callId}/users/leave`, { + method: "POST", + keepalive: true, + headers: { + "Content-Type": "application/json; charset=utf-8", + Accept: "application/json, text/plain, */*", + Authorization: `Bearer ${token}`, + "internxt-version": "0.0.1", + "internxt-client": "internxt-meet", + }, + body, + }); + }; + /** * Handles the action to leave the meeting immediately. * This is triggered when the user clicks the "X" button. @@ -230,6 +263,7 @@ class Conference extends AbstractConference { * @returns {void} */ _leaveMeeting(): void { + window.removeEventListener("pagehide", this._handlePageHide, true); this.props.dispatch(hangup(true, this.props.roomId)); } From 048598aa6050a951f32e5061d9627d16b7069432 Mon Sep 17 00:00:00 2001 From: tamarafinogina Date: Fri, 17 Apr 2026 15:57:50 +0200 Subject: [PATCH 02/11] switch to new _connectInternal --- react/features/base/conference/actionTypes.ts | 2 +- react/features/base/connection/actionTypes.ts | 10 ++++ react/features/base/connection/actions.any.ts | 47 +++++++++++++------ react/features/base/connection/actions.web.ts | 38 ++++----------- 4 files changed, 52 insertions(+), 45 deletions(-) diff --git a/react/features/base/conference/actionTypes.ts b/react/features/base/conference/actionTypes.ts index 4b73da9401b5..5b14cbe313a8 100644 --- a/react/features/base/conference/actionTypes.ts +++ b/react/features/base/conference/actionTypes.ts @@ -136,7 +136,7 @@ export const CONFERENCE_UNIQUE_ID_SET = 'CONFERENCE_UNIQUE_ID_SET'; * } * } */ -export const E2E_RTT_CHANGED = 'E2E_RTT_CHANGED' +export const E2E_RTT_CHANGED = 'E2E_RTT_CHANGED'; /** * The type of (redux) action which signals that a conference will be initialized. diff --git a/react/features/base/connection/actionTypes.ts b/react/features/base/connection/actionTypes.ts index f11ac2d3e8ff..a0279cbcc3a4 100644 --- a/react/features/base/connection/actionTypes.ts +++ b/react/features/base/connection/actionTypes.ts @@ -51,6 +51,16 @@ export const CONNECTION_PROPERTIES_UPDATED = 'CONNECTION_PROPERTIES_UPDATED'; */ export const CONNECTION_WILL_CONNECT = 'CONNECTION_WILL_CONNECT'; +/** + * The type of (redux) action which signals that the token for a connection is expired. + * + * { + * type: CONNECTION_TOKEN_EXPIRED, + * connection: JitsiConnection + * } + */ +export const CONNECTION_TOKEN_EXPIRED = 'CONNECTION_TOKEN_EXPIRED'; + /** * The type of (redux) action which sets the location URL of the application, * connection, conference, etc. diff --git a/react/features/base/connection/actions.any.ts b/react/features/base/connection/actions.any.ts index 3641fa6f2bef..4a4c07ad3650 100644 --- a/react/features/base/connection/actions.any.ts +++ b/react/features/base/connection/actions.any.ts @@ -17,13 +17,14 @@ import { import { setJoinRoomError } from "../meet/general/store/errors/actions"; import { LocalStorageManager } from "../meet/LocalStorageManager"; import MeetingService from "../meet/services/meeting.service"; -import { clearNewMeetingFlowSession, isNewMeetingFlow } from "../meet/services/sessionStorage.service"; +import { clearNewMeetingFlowSession } from "../meet/services/sessionStorage.service"; import { CONNECTION_DISCONNECTED, CONNECTION_ESTABLISHED, CONNECTION_FAILED, CONNECTION_PROPERTIES_UPDATED, CONNECTION_WILL_CONNECT, + CONNECTION_TOKEN_EXPIRED, SET_LOCATION_URL, SET_PREFER_VISITOR, } from "./actionTypes"; @@ -219,19 +220,7 @@ export function setPreferVisitor(preferVisitor: boolean) { * @param {string} [password] - The XMPP user's password. * @returns {Function} */ -export function _connectInternal({ - id, - password, - name, - lastname, - isAnonymous, -}: { - id?: string; - password?: string; - name?: string; - lastname?: string; - isAnonymous?: boolean; -}) { +export function _connectInternal(id?: string, name?: string, password?: string, isAnonymous?: boolean) { return async (dispatch: IStore["dispatch"], getState: IStore["getState"]) => { const state = getState(); const options = constructOptions(state); @@ -249,7 +238,7 @@ export function _connectInternal({ } const { token: jwt, appId } = await MeetingService.instance.joinCall(room, { name: displayName ?? name ?? "", - lastname: lastname ?? "", + lastname: "Internxt Meet User", anonymous: !!isAnonymous, anonymousId: userUUID, }); @@ -271,6 +260,7 @@ export function _connectInternal({ connection.addEventListener(JitsiConnectionEvents.CONNECTION_FAILED, _onConnectionFailed); connection.addEventListener(JitsiConnectionEvents.CONNECTION_REDIRECTED, _onConnectionRedirected); connection.addEventListener(JitsiConnectionEvents.PROPERTIES_UPDATED, _onPropertiesUpdate); + connection.addEventListener(JitsiConnectionEvents.CONNECTION_TOKEN_EXPIRED, _onTokenExpired); /** * Unsubscribe the connection instance from @@ -365,6 +355,16 @@ export function _connectInternal({ dispatch(redirect(vnode, focusJid, username)); } + /** + * Connection will resume. + * + * @private + * @returns {void} + */ + function _onTokenExpired(): void { + dispatch(_connectionTokenExpired(connection)); + } + /** * Connection properties were updated. * @@ -419,6 +419,23 @@ export function _connectInternal({ function _connectionWillConnect(connection: Object) { return { type: CONNECTION_WILL_CONNECT, + connection, + }; +} + +/** + * Create an action for when a connection token is expired. + * + * @param {JitsiConnection} connection - The {@code JitsiConnection} token is expired. + * @private + * @returns {{ + * type: CONNECTION_TOKEN_EXPIRED, + * connection: JitsiConnection + * }} + */ +function _connectionTokenExpired(connection: Object) { + return { + type: CONNECTION_TOKEN_EXPIRED, connection }; } diff --git a/react/features/base/connection/actions.web.ts b/react/features/base/connection/actions.web.ts index eba0caff7d13..8628be46266e 100644 --- a/react/features/base/connection/actions.web.ts +++ b/react/features/base/connection/actions.web.ts @@ -44,9 +44,11 @@ export function connect(id?: string, password?: string) { const state = getState(); const { jwt } = state["features/base/jwt"]; const { iAmRecorder, iAmSipGateway } = state["features/base/config"]; - // TODO: CHECK WHY USER REDUCER IS NULL IN THIS POINT, initializers are not executing as expected - // const { user } = state["features/user"]; + const user = LocalStorageManager.instance.getUser(); + const isAnonymous: boolean = !user; + const name = user?.name || 'Internxt Meet User'; + if (!iAmRecorder && !iAmSipGateway && isVpaasMeeting(state)) { return dispatch(getCustomerDetails()) .then(() => { @@ -54,25 +56,11 @@ export function connect(id?: string, password?: string) { return getJaasJWT(state); } }) - .then((j) => j && dispatch(setJWT(j))) - .then(() => - dispatch( - _connectInternal({ - id, - password, - name: user?.name, - lastname: user?.lastname, - isAnonymous: !user, - }) - ) - ) - // latest jitsi changes, test if not works current ones - // .then(j => { - // j && dispatch(setJWT(j)); + .then(j => { + j && dispatch(setJWT(j)); - // return dispatch(_connectInternal(id, password)); - // }) - .catch(e => { + return dispatch(_connectInternal(id, name, password, isAnonymous)); + }).catch(e => { logger.error('Connection error', e); }); } @@ -88,15 +76,7 @@ export function connect(id?: string, password?: string) { password = passwordOverride; // eslint-disable-line no-param-reassign } - return dispatch( - _connectInternal({ - id, - password, - name: user?.name, - lastname: user?.lastname, - isAnonymous: !user, - }) - ); + return dispatch(_connectInternal(id, name, password, isAnonymous)); }; } From 5f80d23b45357d97f0005171fa455c094057e3e4 Mon Sep 17 00:00:00 2001 From: tamarafinogina Date: Wed, 22 Apr 2026 15:12:23 +0200 Subject: [PATCH 03/11] remove before unload event handler --- .env.template | 3 +- package.json | 2 +- react/features/base/connection/actions.any.ts | 10 ++++- react/features/base/connection/actions.web.ts | 7 +-- .../base/connection/middleware.web.ts | 43 ++++++------------- .../base/meet/views/Conference/Conference.tsx | 21 +++------ webpack.config.js | 3 +- 7 files changed, 36 insertions(+), 53 deletions(-) diff --git a/.env.template b/.env.template index 942869cdec63..a1ec50a56ef2 100644 --- a/.env.template +++ b/.env.template @@ -4,4 +4,5 @@ MEET_API_URL=http://meet-server:3006 CRYPTO_SECRET=6KYQBP847D4ATSFA MAGIC_IV=d139cb9a2cd17092e79e1861cf9d7023 MAGIC_SALT=38dce0391b49efba88dbc8c39ebf868f0267eb110bb0012ab27dc52a528d61b1d1ed9d76f400ff58e3240028442b1eab9bb84e111d9dadd997982dbde9dbd25e -JITSI_APP_ID=vpaas-magic-cookie-04a19c25aaab448c9cf74516ffb5ebf2 \ No newline at end of file +JITSI_APP_ID=vpaas-magic-cookie-04a19c25aaab448c9cf74516ffb5ebf2 +WEBPACK_DEV_SERVER_PROXY_TARGET=https://meet.internxt.com \ No newline at end of file diff --git a/package.json b/package.json index 61610da314f6..af3ed1efd5e1 100644 --- a/package.json +++ b/package.json @@ -264,7 +264,7 @@ "validate": "npm ls", "tsc-test:web": "tsc --project tsconfig.web.json --listFilesOnly | grep -v node_modules | grep native", "tsc-test:native": "tsc --project tsconfig.native.json --listFilesOnly | grep -v node_modules | grep web", - "start": "make dev", + "start": "WEBPACK_DEV_SERVER_PROXY_TARGET=https://meet.internxt.com make dev", "test": "vitest run", "test:ui": "vitest --ui", "test:coverage": "vitest run --coverage", diff --git a/react/features/base/connection/actions.any.ts b/react/features/base/connection/actions.any.ts index 4a4c07ad3650..8e6c57f95425 100644 --- a/react/features/base/connection/actions.any.ts +++ b/react/features/base/connection/actions.any.ts @@ -220,7 +220,13 @@ export function setPreferVisitor(preferVisitor: boolean) { * @param {string} [password] - The XMPP user's password. * @returns {Function} */ -export function _connectInternal(id?: string, name?: string, password?: string, isAnonymous?: boolean) { +export function _connectInternal( + id?: string, + name?: string, + lastname?: string, + password?: string, + isAnonymous?: boolean, +) { return async (dispatch: IStore["dispatch"], getState: IStore["getState"]) => { const state = getState(); const options = constructOptions(state); @@ -238,7 +244,7 @@ export function _connectInternal(id?: string, name?: string, password?: string, } const { token: jwt, appId } = await MeetingService.instance.joinCall(room, { name: displayName ?? name ?? "", - lastname: "Internxt Meet User", + lastname: lastname ?? "", anonymous: !!isAnonymous, anonymousId: userUUID, }); diff --git a/react/features/base/connection/actions.web.ts b/react/features/base/connection/actions.web.ts index 8628be46266e..a5e9987fa8d4 100644 --- a/react/features/base/connection/actions.web.ts +++ b/react/features/base/connection/actions.web.ts @@ -47,7 +47,8 @@ export function connect(id?: string, password?: string) { const user = LocalStorageManager.instance.getUser(); const isAnonymous: boolean = !user; - const name = user?.name || 'Internxt Meet User'; + const name = user?.name; + const lastname = user?.lastname; if (!iAmRecorder && !iAmSipGateway && isVpaasMeeting(state)) { return dispatch(getCustomerDetails()) @@ -59,7 +60,7 @@ export function connect(id?: string, password?: string) { .then(j => { j && dispatch(setJWT(j)); - return dispatch(_connectInternal(id, name, password, isAnonymous)); + return dispatch(_connectInternal(id, name, lastname, password, isAnonymous)); }).catch(e => { logger.error('Connection error', e); }); @@ -76,7 +77,7 @@ export function connect(id?: string, password?: string) { password = passwordOverride; // eslint-disable-line no-param-reassign } - return dispatch(_connectInternal(id, name, password, isAnonymous)); + return dispatch(_connectInternal(id, name, lastname, password, isAnonymous)); }; } diff --git a/react/features/base/connection/middleware.web.ts b/react/features/base/connection/middleware.web.ts index 0bdc10b1e576..44c584076e69 100644 --- a/react/features/base/connection/middleware.web.ts +++ b/react/features/base/connection/middleware.web.ts @@ -1,45 +1,30 @@ -import { redirectToStaticPage } from '../../app/actions.any'; -import { CONFERENCE_WILL_LEAVE } from "../conference/actionTypes"; -import { isLeavingConferenceManually, setLeaveConferenceManually } from "../meet/general/utils/conferenceState"; -import MiddlewareRegistry from "../redux/MiddlewareRegistry"; +import MiddlewareRegistry from '../redux/MiddlewareRegistry'; -import { CONNECTION_DISCONNECTED, CONNECTION_WILL_CONNECT } from "./actionTypes"; +import { CONNECTION_WILL_CONNECT } from './actionTypes'; /** * The feature announced so we can distinguish jibri participants. * * @type {string} */ -export const DISCO_JIBRI_FEATURE = "http://jitsi.org/protocol/jibri"; +export const DISCO_JIBRI_FEATURE = 'http://jitsi.org/protocol/jibri'; -MiddlewareRegistry.register(({ getState, dispatch }) => (next) => (action) => { +MiddlewareRegistry.register(({ getState }) => next => action => { switch (action.type) { - case CONNECTION_WILL_CONNECT: { - const { connection } = action; - const { iAmRecorder } = getState()["features/base/config"]; + case CONNECTION_WILL_CONNECT: { + const { connection } = action; + const { iAmRecorder } = getState()['features/base/config']; - if (iAmRecorder) { - connection.addFeature(DISCO_JIBRI_FEATURE); - } - - // @ts-ignore - APP.connection = connection; - - setLeaveConferenceManually(false); - break; - } - - case CONFERENCE_WILL_LEAVE: { - setLeaveConferenceManually(true); - break; + if (iAmRecorder) { + connection.addFeature(DISCO_JIBRI_FEATURE); } - case CONNECTION_DISCONNECTED: { - setLeaveConferenceManually(false); + // @ts-ignore + APP.connection = connection; - break; - } + break; + } } return next(action); -}); +}); \ No newline at end of file diff --git a/react/features/base/meet/views/Conference/Conference.tsx b/react/features/base/meet/views/Conference/Conference.tsx index 4b5c09558f30..ec64cef01751 100644 --- a/react/features/base/meet/views/Conference/Conference.tsx +++ b/react/features/base/meet/views/Conference/Conference.tsx @@ -139,6 +139,7 @@ class Conference extends AbstractConference { * @returns {void} */ _handlePopState = () => { + console.log("[RELOAD]: Popstate event triggered, handling back/forward navigation"); const { t } = this.props; if (APP.conference.isJoined()) { @@ -146,6 +147,7 @@ class Conference extends AbstractConference { t('dialog.leaveMeetingConfirmation') ); if (confirmLeave) { + window.removeEventListener("pagehide", this._handlePageHide, true); this.props.dispatch(hangup(false, this.props.roomId)); window.history.pushState(null, '', '/'); } else { @@ -165,7 +167,6 @@ class Conference extends AbstractConference { document.title = `${interfaceConfig.APP_NAME}`; this._start(); - window.addEventListener("beforeunload", this._handleBeforeUnload, true); window.addEventListener("popstate", this._handlePopState); window.addEventListener("pagehide", this._handlePageHide, true); } @@ -201,33 +202,21 @@ class Conference extends AbstractConference { FULL_SCREEN_EVENTS.forEach((name) => document.removeEventListener(name, this._onFullScreenChange)); - window.removeEventListener("beforeunload", this._handleBeforeUnload, true); window.removeEventListener("popstate", this._handlePopState); window.removeEventListener("pagehide", this._handlePageHide, true); if (APP.conference.isJoined()) { + window.removeEventListener("pagehide", this._handlePageHide, true); this.props.dispatch(hangup(true, this.props.roomId)); } } /** - * Handler for beforeunload event that shows a confirmation dialog - * when user tries to close the tab or browser during a meeting. - * - * @param {BeforeUnloadEvent} event - The beforeunload event. + * + * Handler for pagehide event that sends a leave call request to the server when the page is being unloaded. * @private * @returns {string} */ - _handleBeforeUnload = (event: BeforeUnloadEvent): string => { - if (APP.conference.isJoined()) { - event.preventDefault(); - event.stopImmediatePropagation(); - - return ''; - } - return ""; - }; - _handlePageHide = (): void => { console.log("[RELOAD]: Page is being hidden, sending leave call request"); diff --git a/webpack.config.js b/webpack.config.js index f7eb53ccd976..c5f885ed5328 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -7,6 +7,7 @@ const process = require("process"); const webpack = require("webpack"); const { BundleAnalyzerPlugin } = require("webpack-bundle-analyzer"); const dotenv = require("dotenv"); +dotenv.config(); /** * The URL of the Jitsi Meet deployment to be proxy to in the context of @@ -279,6 +280,7 @@ function getConfig(options = {}) { * @returns {Object} the dev server configuration. */ function getDevServerConfig() { + console.log('[RELOAD]: Building dev server config with proxy target', devServerProxyTarget); return { client: { overlay: { @@ -349,7 +351,6 @@ module.exports = (_env, argv) => { }), new webpack.DefinePlugin({ "process.env": (() => { - dotenv.config(); const keys = [ "DRIVE_NEW_API_URL", "PAYMENTS_API_URL", From 6381b8e2e4a9e25602414ac9b365ac5868846677 Mon Sep 17 00:00:00 2001 From: tamarafinogina Date: Wed, 22 Apr 2026 15:20:00 +0200 Subject: [PATCH 04/11] remove unneded changes --- react/features/base/connection/actions.any.ts | 20 ++++++++++------ react/features/base/connection/actions.web.ts | 23 +++++++++++++++---- 2 files changed, 31 insertions(+), 12 deletions(-) diff --git a/react/features/base/connection/actions.any.ts b/react/features/base/connection/actions.any.ts index 8e6c57f95425..6101fbeee3c9 100644 --- a/react/features/base/connection/actions.any.ts +++ b/react/features/base/connection/actions.any.ts @@ -220,13 +220,19 @@ export function setPreferVisitor(preferVisitor: boolean) { * @param {string} [password] - The XMPP user's password. * @returns {Function} */ -export function _connectInternal( - id?: string, - name?: string, - lastname?: string, - password?: string, - isAnonymous?: boolean, -) { +export function _connectInternal({ + id, + password, + name, + lastname, + isAnonymous, +}: { + id?: string; + password?: string; + name?: string; + lastname?: string; + isAnonymous?: boolean; +}) { return async (dispatch: IStore["dispatch"], getState: IStore["getState"]) => { const state = getState(); const options = constructOptions(state); diff --git a/react/features/base/connection/actions.web.ts b/react/features/base/connection/actions.web.ts index a5e9987fa8d4..0f1a3649d38c 100644 --- a/react/features/base/connection/actions.web.ts +++ b/react/features/base/connection/actions.web.ts @@ -46,9 +46,6 @@ export function connect(id?: string, password?: string) { const { iAmRecorder, iAmSipGateway } = state["features/base/config"]; const user = LocalStorageManager.instance.getUser(); - const isAnonymous: boolean = !user; - const name = user?.name; - const lastname = user?.lastname; if (!iAmRecorder && !iAmSipGateway && isVpaasMeeting(state)) { return dispatch(getCustomerDetails()) @@ -60,7 +57,15 @@ export function connect(id?: string, password?: string) { .then(j => { j && dispatch(setJWT(j)); - return dispatch(_connectInternal(id, name, lastname, password, isAnonymous)); + return dispatch( + _connectInternal({ + id, + password, + name: user?.name, + lastname: user?.lastname, + isAnonymous: !user, + }) + ); }).catch(e => { logger.error('Connection error', e); }); @@ -77,7 +82,15 @@ export function connect(id?: string, password?: string) { password = passwordOverride; // eslint-disable-line no-param-reassign } - return dispatch(_connectInternal(id, name, lastname, password, isAnonymous)); + return dispatch( + _connectInternal({ + id, + password, + name: user?.name, + lastname: user?.lastname, + isAnonymous: !user, + }) + ); }; } From 44935176f0151e2e493f873028e9a4acda52cd51 Mon Sep 17 00:00:00 2001 From: tamarafinogina Date: Tue, 28 Apr 2026 09:59:54 +0200 Subject: [PATCH 05/11] notifyOnConference enable, add https requests to test proxy exceptions --- react/features/base/conference/middleware.web.ts | 4 +--- webpack.config.js | 6 +++++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/react/features/base/conference/middleware.web.ts b/react/features/base/conference/middleware.web.ts index 619638e3904e..68d4263bc7e1 100644 --- a/react/features/base/conference/middleware.web.ts +++ b/react/features/base/conference/middleware.web.ts @@ -138,9 +138,7 @@ MiddlewareRegistry.register(store => next => action => { Object.values(TRIGGER_READY_TO_CLOSE_REASONS).indexOf(reason) ]; const roomId = room ?? ""; - dispatch(hangup(true, roomId, i18next.t(titlekey) || reason)); - // new jitsi change, test aht does notifyOnConferenceDestruction - // dispatch(hangup(true, i18next.t(titlekey) || reason, notifyOnConferenceDestruction)); + dispatch(hangup(true, roomId, i18next.t(titlekey) || reason, notifyOnConferenceDestruction)); } releaseScreenLock(); diff --git a/webpack.config.js b/webpack.config.js index c5f885ed5328..2ad13bda8058 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -65,7 +65,7 @@ function getBundleAnalyzerPlugin(analyzeBundle, name) { * @returns {string|undefined} If the request is to be served by the proxy * target, undefined; otherwise, the path to the local file to be served. */ -function devServerProxyBypass({ path }) { +function devServerProxyBypass({ path, headers }) { let tpath = path; if (tpath.startsWith("/v1/_cdn/")) { @@ -73,6 +73,10 @@ function devServerProxyBypass({ path }) { tpath = tpath.replace(/\/v1\/_cdn\/[^/]+\//, "/"); } + if (headers?.accept?.includes("text/html")) { + return "/index.html"; + } + if ( tpath.startsWith("/css/") || tpath.startsWith("/doc/") || From e0b290c0050bd3f8ab89533a98fd0ad2ec56be5a Mon Sep 17 00:00:00 2001 From: tamarafinogina Date: Tue, 28 Apr 2026 10:59:51 +0200 Subject: [PATCH 06/11] remove comment --- webpack.config.js | 1 - 1 file changed, 1 deletion(-) diff --git a/webpack.config.js b/webpack.config.js index 2ad13bda8058..89972b80d280 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -284,7 +284,6 @@ function getConfig(options = {}) { * @returns {Object} the dev server configuration. */ function getDevServerConfig() { - console.log('[RELOAD]: Building dev server config with proxy target', devServerProxyTarget); return { client: { overlay: { From 8e4cf6379b4311f88747d01afc66e96b3cff049d Mon Sep 17 00:00:00 2001 From: tamarafinogina Date: Thu, 30 Apr 2026 11:48:37 +0200 Subject: [PATCH 07/11] remove old login --- .env.template | 3 - .github/workflows/publish.yml | 3 - package.json | 2 - react/features/base/meet/__tests__/setup.ts | 40 -- .../services/__tests__/crypto.service.test.ts | 115 ----- .../services/__tests__/keys.service.test.ts | 417 ------------------ .../base/meet/services/auth.service.ts | 66 --- .../base/meet/services/crypto.service.ts | 143 ------ .../base/meet/services/crypto/pgp.service.ts | 31 -- .../base/meet/services/keys.service.ts | 197 --------- .../base/meet/services/types/config.types.ts | 3 - .../base/meet/services/utils/crypto.utils.ts | 7 - .../views/Home/components/auth/SignUpForm.tsx | 125 ------ .../views/Home/hooks/useLoginModal.test.ts | 224 ---------- .../meet/views/Home/hooks/useLoginModal.ts | 159 ------- .../meet/views/Home/hooks/useSignUp.test.ts | 286 ------------ .../base/meet/views/Home/hooks/useSignUp.ts | 107 ----- react/test/setup.ts | 29 -- webpack.config.js | 3 - yarn.lock | 24 +- 20 files changed, 1 insertion(+), 1983 deletions(-) delete mode 100644 react/features/base/meet/services/__tests__/crypto.service.test.ts delete mode 100644 react/features/base/meet/services/__tests__/keys.service.test.ts delete mode 100644 react/features/base/meet/services/crypto.service.ts delete mode 100644 react/features/base/meet/services/crypto/pgp.service.ts delete mode 100644 react/features/base/meet/services/keys.service.ts delete mode 100644 react/features/base/meet/services/utils/crypto.utils.ts delete mode 100644 react/features/base/meet/views/Home/components/auth/SignUpForm.tsx delete mode 100644 react/features/base/meet/views/Home/hooks/useLoginModal.test.ts delete mode 100644 react/features/base/meet/views/Home/hooks/useLoginModal.ts delete mode 100644 react/features/base/meet/views/Home/hooks/useSignUp.test.ts delete mode 100644 react/features/base/meet/views/Home/hooks/useSignUp.ts diff --git a/.env.template b/.env.template index a1ec50a56ef2..0301ef7b3c16 100644 --- a/.env.template +++ b/.env.template @@ -1,8 +1,5 @@ DRIVE_NEW_API_URL=http://drive-server-wip:3004/api PAYMENTS_API_URL=http://payments-server:8003 MEET_API_URL=http://meet-server:3006 -CRYPTO_SECRET=6KYQBP847D4ATSFA -MAGIC_IV=d139cb9a2cd17092e79e1861cf9d7023 -MAGIC_SALT=38dce0391b49efba88dbc8c39ebf868f0267eb110bb0012ab27dc52a528d61b1d1ed9d76f400ff58e3240028442b1eab9bb84e111d9dadd997982dbde9dbd25e JITSI_APP_ID=vpaas-magic-cookie-04a19c25aaab448c9cf74516ffb5ebf2 WEBPACK_DEV_SERVER_PROXY_TARGET=https://meet.internxt.com \ No newline at end of file diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 3beaff740178..27bc411dfe81 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -39,9 +39,6 @@ jobs: echo DRIVE_NEW_API_URL="${{ secrets.DRIVE_NEW_API_URL }}" >> .env echo PAYMENTS_API_URL="${{ secrets.PAYMENTS_API_URL }}" >> .env echo MEET_API_URL="${{ secrets.MEET_API_URL }}" >> .env - echo CRYPTO_SECRET="${{ secrets.CRYPTO_SECRET }}" >> .env - echo MAGIC_IV="${{ secrets.MAGIC_IV }}" >> .env - echo MAGIC_SALT="${{ secrets.MAGIC_SALT }}" >> .env echo JITSI_APP_ID="${{ secrets.JITSI_APP_ID }}" >> .env - name: Build application diff --git a/package.json b/package.json index af3ed1efd5e1..206c22ff7a78 100644 --- a/package.json +++ b/package.json @@ -75,7 +75,6 @@ "focus-visible": "5.1.0", "glob": "11.1.0", "grapheme-splitter": "1.0.4", - "hash-wasm": "=4.11.0", "i18n-iso-countries": "6.8.0", "i18next": "^19.9.2", "i18next-browser-languagedetector": "^6.1.8", @@ -91,7 +90,6 @@ "moment": "2.29.4", "moment-duration-format": "2.2.2", "null-loader": "4.0.1", - "openpgp": "^5.11.1", "optional-require": "1.0.3", "pixelmatch": "5.3.0", "promise.withresolvers": "1.0.3", diff --git a/react/features/base/meet/__tests__/setup.ts b/react/features/base/meet/__tests__/setup.ts index 61d59328e719..633e0334d6c6 100644 --- a/react/features/base/meet/__tests__/setup.ts +++ b/react/features/base/meet/__tests__/setup.ts @@ -1,45 +1,5 @@ import { vi } from 'vitest'; -vi.mock("@openpgp/web-stream-tools", () => ({ - concatUint8Array: vi.fn((arr) => arr), - concat: vi.fn(async (streams) => new Uint8Array()), - stream: { - transform: vi.fn((data, fn) => data), - readToEnd: vi.fn(async () => new Uint8Array()), - slice: vi.fn((stream, begin, end) => stream), - }, - Reader: vi.fn(), - Writer: vi.fn(), -})); - - -vi.mock('openpgp/dist/openpgp.js', () => ({ - default: {}, -})); - - -vi.mock("openpgp", () => ({ - default: { - readKey: vi.fn(), - readPrivateKey: vi.fn(), - readMessage: vi.fn(), - encrypt: vi.fn(), - decrypt: vi.fn(), - generateKey: vi.fn(), - Key: { - fromPublic: vi.fn(), - fromPrivate: vi.fn(), - }, - }, - readKey: vi.fn(), - readPrivateKey: vi.fn(), - readMessage: vi.fn(), - encrypt: vi.fn(), - decrypt: vi.fn(), - generateKey: vi.fn(), -})); - - vi.stubGlobal('crypto', { getRandomValues: vi.fn(), subtle: { diff --git a/react/features/base/meet/services/__tests__/crypto.service.test.ts b/react/features/base/meet/services/__tests__/crypto.service.test.ts deleted file mode 100644 index e1e126464df9..000000000000 --- a/react/features/base/meet/services/__tests__/crypto.service.test.ts +++ /dev/null @@ -1,115 +0,0 @@ -import '../../__tests__/setup'; -import { describe, it, expect, beforeEach, vi } from 'vitest'; -import { CryptoService } from '../crypto.service'; -import { ConfigService } from '../config.service'; -import { KeysService } from '../keys.service'; - -vi.mock('../config.service'); -vi.mock('../keys.service'); - -describe('CryptoService', () => { - const mockConfigService = { - get: vi.fn().mockReturnValue('test-secret') - }; - - const mockKeysService = { - generateNewKeysWithEncrypted: vi.fn() - }; - - beforeEach(() => { - vi.clearAllMocks(); - (ConfigService as any).instance = mockConfigService; - (KeysService as any).instance = mockKeysService; - }); - - describe('Password hashing', () => { - it('When generating a hash without salt, then a new salt is generated and hash is created', () => { - const password = 'test-password'; - const result = CryptoService.instance.passToHash({ password }); - - expect(result.salt).toBeDefined(); - expect(result.hash).toBeDefined(); - expect(result.salt.length).toBe(32); // 128/8 = 16 bytes = 32 hex chars - expect(result.hash.length).toBe(64); // 256/8 = 32 bytes = 64 hex chars - }); - - it('When generating a hash with provided salt, then the provided salt is used', () => { - const password = 'test-password'; - const salt = 'test-salt'; - const result = CryptoService.instance.passToHash({ password, salt }); - - expect(result.salt).toBe(salt); - expect(result.hash).toBeDefined(); - }); - }); - - describe('Text encryption and decryption', () => { - it('When encrypting text, then the encrypted text can be decrypted back', () => { - const originalText = 'test-text'; - const encrypted = CryptoService.instance.encryptText(originalText); - const decrypted = CryptoService.instance.decryptText(encrypted); - - expect(encrypted).not.toBe(originalText); - expect(decrypted).toBe(originalText); - }); - - it('When encrypting text with a specific key, then the encrypted text can be decrypted with the same key', () => { - const originalText = 'test-text'; - const key = 'test-key'; - const encrypted = CryptoService.instance.encryptTextWithKey(originalText, key); - const decrypted = CryptoService.instance.decryptTextWithKey(encrypted, key); - - expect(encrypted).not.toBe(originalText); - expect(decrypted).toBe(originalText); - }); - }); - - describe('Key generation', () => { - it('When generating keys, then the crypto provider returns the expected structure', async () => { - const mockKeys = { - privateKeyArmoredEncrypted: 'encrypted-private', - publicKeyArmored: 'public', - revocationCertificate: 'revocation' - }; - - mockKeysService.generateNewKeysWithEncrypted.mockResolvedValue(mockKeys); - - const password = 'test-password'; - const keys = await CryptoService.cryptoProvider.generateKeys(password); - - expect(keys).toEqual({ - privateKeyEncrypted: mockKeys.privateKeyArmoredEncrypted, - publicKey: mockKeys.publicKeyArmored, - revocationCertificate: mockKeys.revocationCertificate, - ecc: { - publicKey: mockKeys.publicKeyArmored, - privateKeyEncrypted: mockKeys.privateKeyArmoredEncrypted - }, - kyber: { - publicKey: null, - privateKeyEncrypted: null - } - }); - }); - }); - - describe('Password hash encryption', () => { - it('When encrypting password hash, then the crypto provider uses the correct process', () => { - const password = 'test-password'; - const encryptedSalt = 'encrypted-salt'; - const decryptedSalt = 'decrypted-salt'; - const hash = 'test-hash'; - - vi.spyOn(CryptoService.instance, 'decryptText').mockReturnValue(decryptedSalt); - vi.spyOn(CryptoService.instance, 'passToHash').mockReturnValue({ salt: decryptedSalt, hash }); - vi.spyOn(CryptoService.instance, 'encryptText').mockReturnValue('encrypted-hash'); - - const result = CryptoService.cryptoProvider.encryptPasswordHash(password, encryptedSalt); - - expect(CryptoService.instance.decryptText).toHaveBeenCalledWith(encryptedSalt); - expect(CryptoService.instance.passToHash).toHaveBeenCalledWith({ password, salt: decryptedSalt }); - expect(CryptoService.instance.encryptText).toHaveBeenCalledWith(hash); - expect(result).toBe('encrypted-hash'); - }); - }); -}); \ No newline at end of file diff --git a/react/features/base/meet/services/__tests__/keys.service.test.ts b/react/features/base/meet/services/__tests__/keys.service.test.ts deleted file mode 100644 index 76a3983fbe14..000000000000 --- a/react/features/base/meet/services/__tests__/keys.service.test.ts +++ /dev/null @@ -1,417 +0,0 @@ -import * as aesModule from "@internxt/lib"; -import { DecryptMessageResult, WebStream } from "openpgp"; -import { beforeEach, describe, expect, it, vi } from "vitest"; -import * as pgpService from "../crypto/pgp.service"; -import { KeysService } from "../keys.service"; -import { - BadEncodedPrivateKeyError, - CorruptedEncryptedPrivateKeyError, - KeysDoNotMatchError, - WrongIterationsToEncryptPrivateKeyError, -} from "../types/keys.types"; -import * as cryptoUtils from "../utils/crypto.utils"; - -vi.mock("@internxt/lib", () => ({ - aes: { - encrypt: vi.fn().mockImplementation((data, password) => `encrypted-${data}-with-${password}`), - decrypt: vi.fn().mockImplementation((data, password, iterations) => { - if (iterations === 9999) throw new Error("Wrong iterations"); - - if (data.startsWith("corrupted")) { - throw new Error("Decryption failed"); - } - - if (data.startsWith("encrypted-")) { - return data.replace("encrypted-", "").split("-with-")[0]; - } - - return `decrypted-${data}`; - }), - }, -})); - -vi.mock("../utils/crypto.utils", () => ({ - CryptoUtils: { - getAesInit: vi.fn().mockReturnValue({ iv: "test-iv", salt: "test-salt" }), - }, -})); - -vi.mock("../crypto/pgp.service", () => ({ - generateNewKeys: vi.fn().mockResolvedValue({ - privateKeyArmored: "test-private-key", - publicKeyArmored: "test-public-key", - revocationCertificate: "test-revocation-cert", - publicKyberKeyBase64: "test-kyber-public-key", - privateKyberKeyBase64: "test-kyber-private-key", - }), - getOpenpgp: vi.fn().mockResolvedValue({ - readKey: vi.fn().mockResolvedValue("mocked-public-key-object"), - readPrivateKey: vi.fn().mockResolvedValue("mocked-private-key-object"), - createMessage: vi.fn().mockResolvedValue("mocked-message-object"), - readMessage: vi.fn().mockResolvedValue("mocked-encrypted-message"), - encrypt: vi.fn().mockResolvedValue("mocked-encrypted-data"), - decrypt: vi.fn().mockImplementation(async ({ message, decryptionKeys }) => { - if (message === "mocked-encrypted-message" && decryptionKeys === "mocked-private-key-object") { - return { data: "validate-keys" }; - } - return { data: "invalid-message" }; - }), - }), -})); - -vi.mock("openpgp", () => ({ - readKey: vi.fn().mockImplementation(async ({ armoredKey }) => { - if (armoredKey === "valid-key" || armoredKey === "test-public-key") { - return "valid-key-object"; - } - throw new Error("Invalid key"); - }), - readPrivateKey: vi.fn(), - createMessage: vi.fn(), - readMessage: vi.fn(), - encrypt: vi.fn(), - decrypt: vi.fn(), - generateKey: vi.fn().mockResolvedValue({ - privateKey: "generated-private-key", - publicKey: "generated-public-key", - revocationCertificate: "generated-revocation-cert", - }), -})); - -describe("KeysService", () => { - const mockPassword = "test-password"; - let keysService: KeysService; - - beforeEach(() => { - vi.clearAllMocks(); - keysService = new KeysService(); - }); - - describe("getKeys", () => { - it("should generate and encrypt keys with correct format", async () => { - const result = await keysService.getKeys(mockPassword); - - expect(pgpService.generateNewKeys).toHaveBeenCalled(); - expect(cryptoUtils.CryptoUtils.getAesInit).toHaveBeenCalled(); - expect(aesModule.aes.encrypt).toHaveBeenCalledWith( - "test-private-key", - mockPassword, - cryptoUtils.CryptoUtils.getAesInit() - ); - expect(aesModule.aes.encrypt).toHaveBeenCalledWith( - "test-kyber-private-key", - mockPassword, - cryptoUtils.CryptoUtils.getAesInit() - ); - - expect(result).toEqual({ - privateKeyEncrypted: `encrypted-test-private-key-with-${mockPassword}`, - publicKey: "test-public-key", - revocationCertificate: "test-revocation-cert", - ecc: { - privateKeyEncrypted: `encrypted-test-private-key-with-${mockPassword}`, - publicKey: "test-public-key", - }, - kyber: { - publicKey: "test-kyber-public-key", - privateKeyEncrypted: `encrypted-test-kyber-private-key-with-${mockPassword}`, - }, - }); - }); - }); - - describe("parseAndDecryptUserKeys", () => { - it("should correctly parse and decrypt user keys with both ECC and Kyber keys", () => { - const decryptSpy = vi.spyOn(keysService, "decryptPrivateKey"); - - const privateKey = "test-private-key"; - const kyberPrivateKey = "test-kyber-private-key"; - - decryptSpy.mockReturnValueOnce(privateKey); - decryptSpy.mockReturnValueOnce(kyberPrivateKey); - - const mockUserSettings = { - privateKey: "encrypted-private-key", - publicKey: "legacy-public-key", - keys: { - ecc: { - publicKey: "test-public-key", - privateKey: "encrypted-ecc-private-key", - }, - kyber: { - publicKey: "test-kyber-public-key", - privateKey: "encrypted-kyber-private-key", - }, - }, - } as any; - - const result = keysService.parseAndDecryptUserKeys(mockUserSettings, mockPassword); - - expect(decryptSpy).toHaveBeenCalledWith("encrypted-private-key", mockPassword); - expect(decryptSpy).toHaveBeenCalledWith("encrypted-kyber-private-key", mockPassword); - - expect(result).toEqual({ - publicKey: "test-public-key", - privateKey: Buffer.from(privateKey).toString("base64"), - publicKyberKey: "test-kyber-public-key", - privateKyberKey: kyberPrivateKey, - }); - - decryptSpy.mockRestore(); - }); - - it("should correctly parse and decrypt user keys with only ECC keys", () => { - const decryptSpy = vi.spyOn(keysService, "decryptPrivateKey"); - decryptSpy.mockReturnValueOnce("test-private-key"); - - const mockUserSettings = { - privateKey: "encrypted-private-key", - publicKey: "legacy-public-key", - keys: { - ecc: { - publicKey: "test-public-key", - privateKey: "encrypted-ecc-private-key", - }, - }, - } as any; - - const result = keysService.parseAndDecryptUserKeys(mockUserSettings, mockPassword); - - expect(decryptSpy).toHaveBeenCalledWith("encrypted-private-key", mockPassword); - - expect(result).toEqual({ - publicKey: "test-public-key", - privateKey: Buffer.from("test-private-key").toString("base64"), - publicKyberKey: "", - privateKyberKey: "", - }); - - decryptSpy.mockRestore(); - }); - - it("should correctly parse and decrypt user keys with only legacy keys", () => { - const decryptSpy = vi.spyOn(keysService, "decryptPrivateKey"); - decryptSpy.mockReturnValueOnce("test-private-key"); - - const mockUserSettings = { - privateKey: "encrypted-private-key", - publicKey: "legacy-public-key", - } as any; - - const result = keysService.parseAndDecryptUserKeys(mockUserSettings, mockPassword); - - expect(decryptSpy).toHaveBeenCalledWith("encrypted-private-key", mockPassword); - - expect(result).toEqual({ - publicKey: "legacy-public-key", - privateKey: Buffer.from("test-private-key").toString("base64"), - publicKyberKey: "", - privateKyberKey: "", - }); - - decryptSpy.mockRestore(); - }); - - it("should handle empty private key", () => { - const mockUserSettings = { - privateKey: "", - publicKey: "legacy-public-key", - } as any; - - const result = keysService.parseAndDecryptUserKeys(mockUserSettings, mockPassword); - - expect(result.privateKey).toBe(""); - }); - }); - - describe("encryptPrivateKey", () => { - it("should encrypt private key using AES with correct parameters", () => { - const plainPrivateKey = "plain-private-key"; - const result = keysService.encryptPrivateKey(plainPrivateKey, mockPassword); - - expect(cryptoUtils.CryptoUtils.getAesInit).toHaveBeenCalled(); - expect(aesModule.aes.encrypt).toHaveBeenCalledWith( - plainPrivateKey, - mockPassword, - cryptoUtils.CryptoUtils.getAesInit() - ); - expect(result).toBe(`encrypted-${plainPrivateKey}-with-${mockPassword}`); - }); - }); - - describe("decryptPrivateKey", () => { - it("should return empty string for empty or too short keys", () => { - const originalMinLength = keysService.MINIMAL_ENCRYPTED_KEY_LEN; - Object.defineProperty(keysService, "MINIMAL_ENCRYPTED_KEY_LEN", { value: 10 }); - - expect(keysService.decryptPrivateKey("", mockPassword)).toBe(""); - expect(keysService.decryptPrivateKey("short", mockPassword)).toBe(""); - - Object.defineProperty(keysService, "MINIMAL_ENCRYPTED_KEY_LEN", { value: originalMinLength }); - expect(aesModule.aes.decrypt).not.toHaveBeenCalled(); - }); - - it("should decrypt valid private key", () => { - const originalMinLength = keysService.MINIMAL_ENCRYPTED_KEY_LEN; - Object.defineProperty(keysService, "MINIMAL_ENCRYPTED_KEY_LEN", { value: 10 }); - - const encryptedKey = `encrypted-test-private-key-with-${mockPassword}`; - - const result = keysService.decryptPrivateKey(encryptedKey, mockPassword); - - expect(aesModule.aes.decrypt).toHaveBeenCalledWith(encryptedKey, mockPassword); - expect(result).toBe("test-private-key"); - - Object.defineProperty(keysService, "MINIMAL_ENCRYPTED_KEY_LEN", { value: originalMinLength }); - }); - - it("should throw CorruptedEncryptedPrivateKeyError when decryption fails", () => { - const originalMinLength = keysService.MINIMAL_ENCRYPTED_KEY_LEN; - Object.defineProperty(keysService, "MINIMAL_ENCRYPTED_KEY_LEN", { value: 10 }); - - const corruptedKey = "corrupted-key".padEnd(150, "x"); - - expect(() => keysService.decryptPrivateKey(corruptedKey, mockPassword)).toThrow( - CorruptedEncryptedPrivateKeyError - ); - - Object.defineProperty(keysService, "MINIMAL_ENCRYPTED_KEY_LEN", { value: originalMinLength }); - }); - }); - - describe("assertPrivateKeyIsValid", () => { - it("should throw WrongIterationsToEncryptPrivateKeyError when key was encrypted with wrong iterations", async () => { - vi.mocked(aesModule.aes.decrypt).mockImplementationOnce((data, password, iterations) => { - if (iterations === 9999) return "some-value"; - throw new Error("Should not reach here"); - }); - - await expect(keysService.assertPrivateKeyIsValid("valid-key", mockPassword)).rejects.toThrow( - WrongIterationsToEncryptPrivateKeyError - ); - - expect(aesModule.aes.decrypt).toHaveBeenCalledWith("valid-key", mockPassword, 9999); - }); - - it("should throw CorruptedEncryptedPrivateKeyError when key cannot be decrypted", async () => { - vi.mocked(aesModule.aes.decrypt).mockImplementationOnce((data, password, iterations) => { - if (iterations === 9999) throw new Error("Invalid iterations"); - return "should-not-reach-here"; - }); - - const spyDecryptPrivateKey = vi.spyOn(keysService, "decryptPrivateKey").mockImplementationOnce(() => { - throw new Error("Cannot decrypt"); - }); - - await expect(keysService.assertPrivateKeyIsValid("corrupted-key", mockPassword)).rejects.toThrow( - CorruptedEncryptedPrivateKeyError - ); - - expect(spyDecryptPrivateKey).toHaveBeenCalledWith("corrupted-key", mockPassword); - }); - - it("should throw BadEncodedPrivateKeyError when key has invalid format", async () => { - vi.mocked(aesModule.aes.decrypt).mockImplementationOnce((data, password, iterations) => { - if (iterations === 9999) throw new Error("Invalid iterations"); - return "should-not-reach-here"; - }); - - const spyDecryptPrivateKey = vi - .spyOn(keysService, "decryptPrivateKey") - .mockReturnValueOnce("invalid-format-key"); - const spyIsValidKey = vi.spyOn(keysService, "isValidKey").mockResolvedValueOnce(false); - - await expect(keysService.assertPrivateKeyIsValid("invalid-format-key", mockPassword)).rejects.toThrow( - BadEncodedPrivateKeyError - ); - - expect(spyDecryptPrivateKey).toHaveBeenCalledWith("invalid-format-key", mockPassword); - expect(spyIsValidKey).toHaveBeenCalledWith("invalid-format-key"); - }); - - it("should not throw any error when key is valid", async () => { - vi.mocked(aesModule.aes.decrypt).mockImplementationOnce((data, password, iterations) => { - if (iterations === 9999) throw new Error("Invalid iterations"); - return "should-not-reach-here"; - }); - - const spyDecryptPrivateKey = vi - .spyOn(keysService, "decryptPrivateKey") - .mockReturnValueOnce("valid-format-key"); - const spyIsValidKey = vi.spyOn(keysService, "isValidKey").mockResolvedValueOnce(true); - - await expect(keysService.assertPrivateKeyIsValid("valid-key", mockPassword)).resolves.not.toThrow(); - - expect(spyDecryptPrivateKey).toHaveBeenCalledWith("valid-key", mockPassword); - expect(spyIsValidKey).toHaveBeenCalledWith("valid-format-key"); - }); - }); - - describe("assertValidateKeys", () => { - it("should not throw error when keys match and can encrypt/decrypt", async () => { - const mockPrivateKey = "test-private-key"; - const mockPublicKey = "test-public-key"; - - await expect(keysService.assertValidateKeys(mockPrivateKey, mockPublicKey)).resolves.not.toThrow(); - - const mockOpenpgp = await pgpService.getOpenpgp(); - - expect(mockOpenpgp.readKey).toHaveBeenCalledWith({ armoredKey: mockPublicKey }); - expect(mockOpenpgp.readPrivateKey).toHaveBeenCalledWith({ armoredKey: mockPrivateKey }); - expect(mockOpenpgp.createMessage).toHaveBeenCalledWith({ text: "validate-keys" }); - expect(mockOpenpgp.encrypt).toHaveBeenCalled(); - expect(mockOpenpgp.readMessage).toHaveBeenCalled(); - expect(mockOpenpgp.decrypt).toHaveBeenCalled(); - }); - - it("should throw KeysDoNotMatchError when decrypted message does not match original", async () => { - const mockPrivateKey = "invalid-private-key"; - const mockPublicKey = "test-public-key"; - - const mockOpenpgp = await pgpService.getOpenpgp(); - vi.mocked(mockOpenpgp.decrypt).mockResolvedValueOnce({ - data: "different-message", - } as unknown as DecryptMessageResult & { data: WebStream }); - - await expect(keysService.assertValidateKeys(mockPrivateKey, mockPublicKey)).rejects.toThrow( - KeysDoNotMatchError - ); - }); - }); - - describe("isValidKey", () => { - it("should return true for a valid key", async () => { - const result = await keysService.isValidKey("valid-key"); - - expect(result).toBe(true); - }); - - it("should return false for an invalid key", async () => { - const result = await keysService.isValidKey("invalid-key"); - - expect(result).toBe(false); - }); - }); - - describe("generateNewKeysWithEncrypted", () => { - it("should generate and encrypt new keys correctly", async () => { - const mockPrivateKey = "generated-private-key"; - - const spyEncryptPrivateKey = vi - .spyOn(keysService, "encryptPrivateKey") - .mockReturnValueOnce("encrypted-generated-private-key"); - - const result = await keysService.generateNewKeysWithEncrypted(mockPassword); - - expect(spyEncryptPrivateKey).toHaveBeenCalledWith(mockPrivateKey, mockPassword); - - expect(result).toEqual({ - privateKeyArmored: mockPrivateKey, - privateKeyArmoredEncrypted: "encrypted-generated-private-key", - publicKeyArmored: Buffer.from("generated-public-key").toString("base64"), - revocationCertificate: Buffer.from("generated-revocation-cert").toString("base64"), - }); - }); - }); -}); - diff --git a/react/features/base/meet/services/auth.service.ts b/react/features/base/meet/services/auth.service.ts index 8cc0026bb5a7..c63db8d64996 100644 --- a/react/features/base/meet/services/auth.service.ts +++ b/react/features/base/meet/services/auth.service.ts @@ -1,74 +1,8 @@ -import { LoginDetails } from "@internxt/sdk"; -import { CryptoService } from "./crypto.service"; -import { KeysService } from "./keys.service"; import { SdkManager } from "./sdk-manager.service"; -import { LoginCredentials } from "./types/command.types"; export class AuthService { public static readonly instance: AuthService = new AuthService(); - /** - * Login with user credentials and returns its tokens and properties - * @param email The user's email - * @param password The user's password - * @param twoFactorCode (Optional) The temporal two factor auth code - * @returns The user's properties and the tokens needed for auth - * @async - **/ - public doLogin = async (email: string, password: string, twoFactorCode?: string): Promise => { - const authClient = SdkManager.instance.getNewAuth(); - const loginDetails: LoginDetails = { - email: email.toLowerCase(), - password: password, - tfaCode: twoFactorCode, - }; - - const data = await authClient.login(loginDetails, CryptoService.cryptoProvider); - const { user, token, newToken } = data; - const { privateKey, publicKey } = user; - - const plainPrivateKeyInBase64 = privateKey - ? Buffer.from(KeysService.instance.decryptPrivateKey(privateKey, password)).toString("base64") - : ""; - - if (privateKey) { - await KeysService.instance.assertPrivateKeyIsValid(privateKey, password); - await KeysService.instance.assertValidateKeys( - Buffer.from(plainPrivateKeyInBase64, "base64").toString(), - Buffer.from(publicKey, "base64").toString() - ); - } - - const clearMnemonic = CryptoService.instance.decryptTextWithKey(user.mnemonic, password); - - const clearUser = { - ...user, - mnemonic: clearMnemonic, - privateKey: plainPrivateKeyInBase64, - }; - return { - user: clearUser, - token: token, - newToken: newToken, - mnemonic: clearMnemonic, - }; - }; - - /** - * Checks from user's security details if it has enabled two factor auth - * @param email The user's email - * @throws {Error} If auth.securityDetails endpoint fails - * @returns True if user has enabled two factor auth - * @async - **/ - public is2FANeeded = async (email: string): Promise => { - const authClient = SdkManager.instance.getNewAuth(); - const securityDetails = await authClient.securityDetails(email).catch((error) => { - throw new Error(error.message ?? "Login error"); - }); - return securityDetails.tfaEnabled; - }; - /** * Obtains the current logged in user * diff --git a/react/features/base/meet/services/crypto.service.ts b/react/features/base/meet/services/crypto.service.ts deleted file mode 100644 index 765f3e7d6b27..000000000000 --- a/react/features/base/meet/services/crypto.service.ts +++ /dev/null @@ -1,143 +0,0 @@ -import { CryptoProvider } from "@internxt/sdk"; -import { Keys, Password } from "@internxt/sdk/dist/auth"; -import crypto from "crypto"; -import { ConfigService } from "./config.service"; -import { KeysService } from "./keys.service"; - -export class CryptoService { - public static readonly instance: CryptoService = new CryptoService(); - - public static readonly cryptoProvider: CryptoProvider = { - encryptPasswordHash(password: Password, encryptedSalt: string): string { - const salt = CryptoService.instance.decryptText(encryptedSalt); - const hashObj = CryptoService.instance.passToHash({ password, salt }); - return CryptoService.instance.encryptText(hashObj.hash); - }, - async generateKeys(password: Password): Promise { - const { privateKeyArmoredEncrypted, publicKeyArmored, revocationCertificate } = - await KeysService.instance.generateNewKeysWithEncrypted(password); - const keys: Keys = { - privateKeyEncrypted: privateKeyArmoredEncrypted, - publicKey: publicKeyArmored, - revocationCertificate: revocationCertificate, - ecc: { - publicKey: publicKeyArmored, - privateKeyEncrypted: privateKeyArmoredEncrypted, - }, - kyber: { - publicKey: null, - privateKeyEncrypted: null, - }, - }; - return keys; - }, - }; - - /** - * Generates the hash for a password, if salt is provided it uses it, in other case it is generated from crypto - * @param passObject The object containing the password and an optional salt hex encoded - * @returns The hashed password and the salt - **/ - public passToHash = (passObject: { password: string; salt?: string | null }): { salt: string; hash: string } => { - const salt = passObject.salt ? passObject.salt : crypto.randomBytes(128 / 8).toString("hex"); - const hash = crypto - .pbkdf2Sync(passObject.password, Buffer.from(salt, "hex") as any, 10000, 256 / 8, "sha1") - .toString("hex"); - const hashedObjetc = { - salt, - hash, - }; - - return hashedObjetc; - }; - - /** - * Encrypts a plain message into an AES encrypted text using APP_CRYPTO_SECRET value from env - * @param textToEncrypt The plain text to be encrypted - * @returns The encrypted string in 'hex' encoding - **/ - public encryptText = (textToEncrypt: string): string => { - const APP_CRYPTO_SECRET = ConfigService.instance.get("CRYPTO_SECRET"); - return this.encryptTextWithKey(textToEncrypt, APP_CRYPTO_SECRET); - }; - - /** - * Decrypts an AES encrypted text using APP_CRYPTO_SECRET value from env - * @param encryptedText The AES encrypted text in 'HEX' encoding - * @returns The decrypted string in 'utf8' encoding - **/ - public decryptText = (encryptedText: string): string => { - const APP_CRYPTO_SECRET = ConfigService.instance.get("CRYPTO_SECRET"); - return this.decryptTextWithKey(encryptedText, APP_CRYPTO_SECRET); - }; - - /** - * Encrypts a plain message into an AES encrypted text using a secret. - * [Crypto.JS compatible]: - * First 8 bytes are reserved for 'Salted__', next 8 bytes are the salt, and the rest is aes content - * @param textToEncrypt The plain text to be encrypted - * @param secret The secret used to encrypt - * @returns The encrypted private string in 'hex' encoding - **/ - public encryptTextWithKey = (textToEncrypt: string, secret: string) => { - const salt = crypto.randomBytes(8); - const { key, iv } = this.getKeyAndIvFrom(secret, salt); - - const cipher = crypto.createCipheriv("aes-256-cbc", key as any, iv as any); - - const encrypted = Buffer.concat([cipher.update(textToEncrypt, "utf8") as any, cipher.final() as any]); - - /* CryptoJS applies the OpenSSL format for the ciphertext, i.e. the encrypted data starts with the ASCII - encoding of 'Salted__' followed by the salt and then the ciphertext. - Therefore the beginning of the Base64 encoded ciphertext starts always with U2FsdGVkX1 - */ - const openSSLstart = Buffer.from("Salted__"); - - return Buffer.concat([openSSLstart as any, salt as any, encrypted as any]).toString("hex"); - }; - - /** - * Decrypts an AES encrypted text using a secret. - * [Crypto.JS compatible]: - * First 8 bytes are reserved for 'Salted__', next 8 bytes are the salt, and the rest is aes content - * @param encryptedText The AES encrypted text in 'HEX' encoding - * @param secret The secret used to encrypt - * @returns The decrypted string in 'utf8' encoding - **/ - public decryptTextWithKey = (encryptedText: string, secret: string) => { - const cypherText = Buffer.from(encryptedText, "hex"); - - const salt = cypherText.subarray(8, 16); - const { key, iv } = this.getKeyAndIvFrom(secret, salt); - - const decipher = crypto.createDecipheriv("aes-256-cbc", key as any, iv as any); - - const contentsToDecrypt = cypherText.subarray(16); - - return Buffer.concat([decipher.update(contentsToDecrypt as any) as any, decipher.final() as any]).toString("utf8"); - }; - - /** - * Generates the key and the iv by transforming a secret and a salt. - * It will generate the same key and iv if the same secret and salt is used. - * This function is needed to be Crypto.JS compatible and encrypt/decrypt without errors - * @param secret The secret used to encrypt - * @param salt The salt used to encrypt - * @returns The key and the iv resulted from the secret and the salt combination - **/ - private getKeyAndIvFrom = (secret: string, salt: Buffer) => { - const TRANSFORM_ROUNDS = 3; - const password = Buffer.concat([Buffer.from(secret, "binary") as any, salt as any]); - const md5Hashes: Buffer[] = []; - let digest = password; - - for (let i = 0; i < TRANSFORM_ROUNDS; i++) { - md5Hashes[i] = crypto.createHash("md5").update(digest as any).digest(); - digest = Buffer.concat([md5Hashes[i] as any, password as any]); - } - - const key = Buffer.concat([md5Hashes[0] as any, md5Hashes[1] as any]); - const iv = md5Hashes[2]; - return { key, iv }; - }; -} diff --git a/react/features/base/meet/services/crypto/pgp.service.ts b/react/features/base/meet/services/crypto/pgp.service.ts deleted file mode 100644 index 1278f120f526..000000000000 --- a/react/features/base/meet/services/crypto/pgp.service.ts +++ /dev/null @@ -1,31 +0,0 @@ -import kemBuilder from "@dashlane/pqc-kem-kyber512-browser"; -import { Buffer } from "buffer"; - -export async function getOpenpgp(): Promise { - return import("openpgp"); -} -export async function generateNewKeys(): Promise<{ - privateKeyArmored: string; - publicKeyArmored: string; - revocationCertificate: string; - publicKyberKeyBase64: string; - privateKyberKeyBase64: string; -}> { - const openpgp = await getOpenpgp(); - - const { privateKey, publicKey, revocationCertificate } = await openpgp.generateKey({ - userIDs: [{ email: "inxt@inxt.com" }], - curve: "ed25519", - }); - - const kem = await kemBuilder(); - const { publicKey: publicKyberKey, privateKey: privateKyberKey } = await kem.keypair(); - - return { - privateKeyArmored: privateKey, - publicKeyArmored: Buffer.from(publicKey).toString("base64"), - revocationCertificate: Buffer.from(revocationCertificate).toString("base64"), - publicKyberKeyBase64: Buffer.from(publicKyberKey).toString("base64"), - privateKyberKeyBase64: Buffer.from(privateKyberKey).toString("base64"), - }; -} diff --git a/react/features/base/meet/services/keys.service.ts b/react/features/base/meet/services/keys.service.ts deleted file mode 100644 index 7958b0a73467..000000000000 --- a/react/features/base/meet/services/keys.service.ts +++ /dev/null @@ -1,197 +0,0 @@ -import { aes } from '@internxt/lib'; -import { Keys } from "@internxt/sdk"; -import { UserSettings } from "@internxt/sdk/dist/shared/types/userSettings"; -import * as openpgp from "openpgp"; -import { generateNewKeys, getOpenpgp } from "./crypto/pgp.service"; -import { - BadEncodedPrivateKeyError, - CorruptedEncryptedPrivateKeyError, - KeysDoNotMatchError, - WrongIterationsToEncryptPrivateKeyError, -} from "./types/keys.types"; -import { CryptoUtils } from "./utils/crypto.utils"; - -export class KeysService { - public static readonly instance: KeysService = new KeysService(); - MINIMAL_ENCRYPTED_KEY_LEN = 129; - - public async getKeys(password: string): Promise { - const { - privateKeyArmored, - publicKeyArmored, - revocationCertificate, - publicKyberKeyBase64, - privateKyberKeyBase64, - } = await generateNewKeys(); - const encPrivateKey = aes.encrypt(privateKeyArmored, password, CryptoUtils.getAesInit()); - const encPrivateKyberKey = aes.encrypt(privateKyberKeyBase64, password, CryptoUtils.getAesInit()); - - const keys: Keys = { - privateKeyEncrypted: encPrivateKey, - publicKey: publicKeyArmored, - revocationCertificate: revocationCertificate, - ecc: { - privateKeyEncrypted: encPrivateKey, - publicKey: publicKeyArmored, - }, - kyber: { - publicKey: publicKyberKeyBase64, - privateKeyEncrypted: encPrivateKyberKey, - }, - }; - return keys; - } - - public parseAndDecryptUserKeys( - user: UserSettings, - password: string - ): { publicKey: string; privateKey: string; publicKyberKey: string; privateKyberKey: string } { - const decryptedPrivateKey = this.decryptPrivateKey(user.privateKey, password); - const privateKey = user.privateKey ? Buffer.from(decryptedPrivateKey).toString("base64") : ""; - - let privateKyberKey = ""; - if (user.keys?.kyber?.privateKey) { - privateKyberKey = this.decryptPrivateKey(user.keys.kyber.privateKey, password); - } - - const publicKey = user.keys?.ecc?.publicKey ?? user.publicKey; - const publicKyberKey = user.keys?.kyber?.publicKey ?? ""; - - return { publicKey, privateKey, publicKyberKey, privateKyberKey }; - } - - /** - * Checks if a private key can be decrypted with a password, otherwise it throws an error - * @param privateKey The encrypted private key - * @param password The password used to encrypt the private key - * @throws {BadEncodedPrivateKeyError} If the PLAIN private key is base64 encoded (known issue introduced in the past) - * @throws {WrongIterationsToEncryptPrivateKeyError} If the ENCRYPTED private key was encrypted using the wrong iterations number (known issue introduced in the past) - * @throws {CorruptedEncryptedPrivateKeyError} If the ENCRYPTED private key is un-decryptable (corrupted) - * @async - */ - public assertPrivateKeyIsValid = async (privateKey: string, password: string): Promise => { - let privateKeyDecrypted: string | undefined; - - let badIterations = true; - try { - aes.decrypt(privateKey, password, 9999); - } catch { - badIterations = false; - } - if (badIterations === true) throw new WrongIterationsToEncryptPrivateKeyError(); - - let badEncrypted = false; - try { - privateKeyDecrypted = this.decryptPrivateKey(privateKey, password); - } catch { - badEncrypted = true; - } - - let hasValidFormat = false; - try { - if (privateKeyDecrypted !== undefined) { - hasValidFormat = await this.isValidKey(privateKeyDecrypted); - } - } catch { - /* no op */ - } - - if (badEncrypted === true) throw new CorruptedEncryptedPrivateKeyError(); - if (hasValidFormat === false) throw new BadEncodedPrivateKeyError(); - }; - - /** - * Encrypts a private key using a password - * @param privateKey The plain private key - * @param password The password to encrypt - * @returns The encrypted private key - **/ - public encryptPrivateKey = (privateKey: string, password: string): string => { - return aes.encrypt(privateKey, password, CryptoUtils.getAesInit()); - }; - - /** - * Decrypts a private key using a password - * @param privateKey The encrypted private key - * @param password The password used to encrypt the private key - * @returns The decrypted private key - **/ - public decryptPrivateKey = (privateKey: string, password: string): string => { - if (!privateKey || privateKey.length <= this.MINIMAL_ENCRYPTED_KEY_LEN) return ""; - else { - try { - const result = aes.decrypt(privateKey, password); - return result; - } catch (error) { - throw new CorruptedEncryptedPrivateKeyError(); - } - } - }; - - /** - * Checks if a message encrypted with the public key can be decrypted with a private key, otherwise it throws an error - * @param privateKey The plain private key - * @param publicKey The plain public key - * @throws {KeysDoNotMatchError} If the keys can not be used together to encrypt/decrypt a message - * @async - **/ - public assertValidateKeys = async (privateKey: string, publicKey: string): Promise => { - const openpgp = await getOpenpgp(); - const publicKeyArmored = await openpgp.readKey({ armoredKey: publicKey }); - const privateKeyArmored = await openpgp.readPrivateKey({ armoredKey: privateKey }); - - const plainMessage = "validate-keys"; - const originalText = await openpgp.createMessage({ text: plainMessage }); - const encryptedMessage = await openpgp.encrypt({ - message: originalText, - encryptionKeys: publicKeyArmored, - }); - - const decryptedMessage = ( - await openpgp.decrypt({ - message: await openpgp.readMessage({ armoredMessage: encryptedMessage }), - verificationKeys: publicKeyArmored, - decryptionKeys: privateKeyArmored, - }) - ).data; - - if (decryptedMessage !== plainMessage) { - throw new KeysDoNotMatchError(); - } - }; - - /** - * Checks if a pgp key can be read - * @param key The openpgp key to be validated - * @returns True if it can be read, false otherwise - * @async - **/ - public isValidKey = async (key: string): Promise => { - try { - await openpgp.readKey({ armoredKey: key }); - return true; - } catch { - return false; - } - }; - - /** - * Generates pgp keys adding an AES-encrypted private key property by using a password - * @param password The password for encrypting the private key - * @returns The keys { privateKeyArmored, privateKeyArmoredEncrypted, publicKeyArmored, revocationCertificate } - * @async - **/ - public generateNewKeysWithEncrypted = async (password: string) => { - const { privateKey, publicKey, revocationCertificate } = await openpgp.generateKey({ - userIDs: [{ email: "inxt@inxt.com" }], - curve: "ed25519", - }); - - return { - privateKeyArmored: privateKey, - privateKeyArmoredEncrypted: this.encryptPrivateKey(privateKey, password), - publicKeyArmored: Buffer.from(publicKey).toString("base64"), - revocationCertificate: Buffer.from(revocationCertificate).toString("base64"), - }; - }; -} diff --git a/react/features/base/meet/services/types/config.types.ts b/react/features/base/meet/services/types/config.types.ts index ea7ab26b0544..5be9475a81e9 100644 --- a/react/features/base/meet/services/types/config.types.ts +++ b/react/features/base/meet/services/types/config.types.ts @@ -2,8 +2,5 @@ export interface ConfigKeys { readonly DRIVE_NEW_API_URL: string; readonly PAYMENTS_API_URL: string; readonly MEET_API_URL: string; - readonly CRYPTO_SECRET: string; - readonly MAGIC_IV: string; - readonly MAGIC_SALT: string; readonly JITSI_APP_ID: string; } diff --git a/react/features/base/meet/services/utils/crypto.utils.ts b/react/features/base/meet/services/utils/crypto.utils.ts deleted file mode 100644 index a3b491c98264..000000000000 --- a/react/features/base/meet/services/utils/crypto.utils.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { ConfigService } from '../config.service'; - -export class CryptoUtils { - static getAesInit(): { iv: string; salt: string } { - return { iv: ConfigService.instance.get('MAGIC_IV'), salt: ConfigService.instance.get('MAGIC_SALT') }; - } -} diff --git a/react/features/base/meet/views/Home/components/auth/SignUpForm.tsx b/react/features/base/meet/views/Home/components/auth/SignUpForm.tsx deleted file mode 100644 index 6bdbde44804c..000000000000 --- a/react/features/base/meet/views/Home/components/auth/SignUpForm.tsx +++ /dev/null @@ -1,125 +0,0 @@ -import { Button } from "@internxt/ui"; -import { Info } from "@phosphor-icons/react"; -import React from "react"; -import { useForm } from "react-hook-form"; -import { usePasswordStrength } from "../../hooks/usePasswordStrength"; -import { IFormValues } from "../../types"; - -import { ErrorMessage } from "../../../../general/components/ErrorMessage"; -import PasswordStrengthIndicator from "./PasswordIndicator"; -import PasswordInput from "./PasswordInput"; -import TextInput from "./TextInput"; - -const MAX_PASSWORD_LENGTH = 64; - -interface SignupFormProps { - onSubmit: (data: IFormValues) => void; - isSigningUp: boolean; - signupError: string; - translate: (key: string) => string; -} - -export const SignupForm: React.FC = ({ onSubmit, isSigningUp, signupError, translate }) => { - const { - register, - formState: { errors, isSubmitted }, - handleSubmit, - watch, - } = useForm({ - mode: "onChange", - reValidateMode: "onChange", - }); - - const password = watch("password", ""); - const email = watch("email", ""); - - const { isValidPassword, passwordState, showPasswordIndicator, handleShowPasswordIndicator } = usePasswordStrength({ - password, - email, - maxLength: MAX_PASSWORD_LENGTH, - translate, - }); - - return ( -
-
- - -
- handleShowPasswordIndicator(true)} - className={passwordState?.tag || ""} - autoComplete="new-password" - /> - {showPasswordIndicator && passwordState && ( - - )} -
- - - -
- -

- {translate("meet.auth.modal.signup.info.normalText")}{" "} - {translate("meet.auth.modal.signup.info.boldText")}{" "} - - - {translate("meet.auth.modal.signup.info.cta")} - - -

-
- - {signupError && } - - -
-
- ); -}; diff --git a/react/features/base/meet/views/Home/hooks/useLoginModal.test.ts b/react/features/base/meet/views/Home/hooks/useLoginModal.test.ts deleted file mode 100644 index b3b2a851f734..000000000000 --- a/react/features/base/meet/views/Home/hooks/useLoginModal.test.ts +++ /dev/null @@ -1,224 +0,0 @@ -import { act, renderHook } from "@testing-library/react"; -import { useDispatch } from "react-redux"; -import { beforeEach, describe, expect, it, vi } from "vitest"; -import { get8x8BetaJWT } from "../../../../connection/options8x8"; -import "../../../__tests__/setup"; -import { useLocalStorage } from "../../../LocalStorageManager"; -import { AuthService } from "../../../services/auth.service"; -import { useLoginModal } from "./useLoginModal"; - -vi.mock("../../../services/auth.service"); -vi.mock("../../../../connection/options8x8"); -vi.mock("../../../LocalStorageManager"); -vi.mock('react-redux', async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - useDispatch: vi.fn(), - useSelector: vi.fn(), - }; -}); -vi.mock("react-hook-form", () => ({ - useForm: () => ({ - register: vi.fn(), - formState: { errors: {} }, - handleSubmit: vi.fn(), - reset: vi.fn(), - watch: vi.fn(() => ""), - }), -})); - -describe("useLoginModal", () => { - const mockOnClose = vi.fn(); - const mockOnLogin = vi.fn(); - const mockTranslate = vi.fn((key) => key); - const mockSaveCredentials = vi.fn(); - const mockDispatch = vi.fn(); - const mockLoginAction = { - type: "features/authentication/LOGIN_SUCCESS", - payload: { token: "new-token", user: { id: 1 } }, - }; - - beforeEach(() => { - vi.clearAllMocks(); - - (useLocalStorage as any).mockReturnValue({ - saveCredentials: mockSaveCredentials, - }); - - (useDispatch as any).mockReturnValue(mockDispatch); - }); - - describe("Initial state", () => { - it("When the modal is initialized, then it has default values", () => { - const { result } = renderHook(() => - useLoginModal({ onClose: mockOnClose, onLogin: mockOnLogin, translate: mockTranslate }) - ); - - expect(result.current.isLoggingIn).toBe(false); - expect(result.current.showTwoFactor).toBe(false); - expect(result.current.loginError).toBe(""); - }); - }); - - describe("Login process", () => { - it("When logging in with valid credentials without 2FA, then the login completes successfully", async () => { - const mockCredentials = { - newToken: "new-token", - user: { id: 1 }, - mnemonic: "mnemonic", - }; - const mockMeetToken = { - token: "meet-token", - room: "room-id", - }; - - (AuthService.instance.doLogin as any).mockResolvedValue(mockCredentials); - (AuthService.instance.is2FANeeded as any).mockResolvedValue(false); - (get8x8BetaJWT as any).mockResolvedValue(mockMeetToken); - - const { result } = renderHook(() => - useLoginModal({ onClose: mockOnClose, onLogin: mockOnLogin, translate: mockTranslate }) - ); - - await act(async () => { - await result.current.handleLogin({ - email: "test@example.com", - password: "password", - twoFactorCode: "", - }); - }); - - expect(mockSaveCredentials).toHaveBeenCalledWith( - mockCredentials.newToken, - mockCredentials.mnemonic, - mockCredentials.user - ); - - expect(mockDispatch).toHaveBeenCalledWith(expect.objectContaining(mockLoginAction)); - - expect(mockOnLogin).toHaveBeenCalledWith(mockCredentials.newToken); - expect(mockOnClose).toHaveBeenCalled(); - }); - - it("When 2FA is enabled for the user, then the 2FA screen is displayed", async () => { - (AuthService.instance.is2FANeeded as any).mockResolvedValue(true); - - const { result } = renderHook(() => - useLoginModal({ onClose: mockOnClose, onLogin: mockOnLogin, translate: mockTranslate }) - ); - - await act(async () => { - await result.current.handleLogin({ - email: "test@example.com", - password: "password", - twoFactorCode: "", - }); - }); - - expect(result.current.showTwoFactor).toBe(true); - expect(mockOnLogin).not.toHaveBeenCalled(); - expect(mockOnClose).not.toHaveBeenCalled(); - }); - }); - - describe("Error handling", () => { - it("When authenticateUser throws invalidCredentials error, then the error message is displayed", async () => { - (AuthService.instance.doLogin as any).mockRejectedValue(new Error("Any error")); - (AuthService.instance.is2FANeeded as any).mockResolvedValue(false); - - mockTranslate.mockImplementation((key) => { - if (key === "meet.auth.modal.error.invalidCredentials") { - return "meet.auth.modal.error.invalidCredentials"; - } - return key; - }); - - const { result } = renderHook(() => - useLoginModal({ onClose: mockOnClose, onLogin: mockOnLogin, translate: mockTranslate }) - ); - - await act(async () => { - await result.current.handleLogin({ - email: "test@example.com", - password: "wrong-password", - twoFactorCode: "", - }); - }); - - expect(result.current.loginError).toBe("meet.auth.modal.error.invalidCredentials"); - expect(mockOnLogin).not.toHaveBeenCalled(); - expect(mockOnClose).not.toHaveBeenCalled(); - }); - - it("When login credentials are invalid or incomplete, then an error message is displayed", async () => { - const mockCredentials = { - token: "token", - mnemonic: "mnemonic", - }; - - (AuthService.instance.doLogin as any).mockResolvedValue(mockCredentials); - (AuthService.instance.is2FANeeded as any).mockResolvedValue(false); - - mockTranslate.mockImplementation((key) => key); - - const { result } = renderHook(() => - useLoginModal({ onClose: mockOnClose, onLogin: mockOnLogin, translate: mockTranslate }) - ); - - await act(async () => { - await result.current.handleLogin({ - email: "test@example.com", - password: "password", - twoFactorCode: "", - }); - }); - - expect(result.current.loginError).toBe("meet.auth.modal.error.invalidCredentials"); - expect(mockOnLogin).not.toHaveBeenCalled(); - expect(mockOnClose).not.toHaveBeenCalled(); - }); - - it("When an unknown error occurs, then a generic error message is displayed", async () => { - (AuthService.instance.is2FANeeded as any).mockRejectedValue("Unknown error"); - - mockTranslate.mockImplementation((key) => { - if (key === "meet.auth.modal.error.genericError") { - return "meet.auth.modal.error.genericError"; - } - return key; - }); - - const { result } = renderHook(() => - useLoginModal({ onClose: mockOnClose, onLogin: mockOnLogin, translate: mockTranslate }) - ); - - await act(async () => { - await result.current.handleLogin({ - email: "test@example.com", - password: "password", - twoFactorCode: "", - }); - }); - - expect(result.current.loginError).toBe("meet.auth.modal.error.genericError"); - expect(mockOnLogin).not.toHaveBeenCalled(); - expect(mockOnClose).not.toHaveBeenCalled(); - }); - }); - - describe("State management", () => { - it("When resetState is called, then all state values are reset to default", () => { - const { result } = renderHook(() => - useLoginModal({ onClose: mockOnClose, onLogin: mockOnLogin, translate: mockTranslate }) - ); - - act(() => { - result.current.resetState(); - }); - - expect(result.current.showTwoFactor).toBe(false); - expect(result.current.loginError).toBe(""); - }); - }); -}); diff --git a/react/features/base/meet/views/Home/hooks/useLoginModal.ts b/react/features/base/meet/views/Home/hooks/useLoginModal.ts deleted file mode 100644 index 36345e113c49..000000000000 --- a/react/features/base/meet/views/Home/hooks/useLoginModal.ts +++ /dev/null @@ -1,159 +0,0 @@ -import { useCallback, useState } from "react"; -import { useForm } from "react-hook-form"; -import { useDispatch } from "react-redux"; - -import { loginSuccess } from "../../../general/store/auth/actions"; -import { setRoomID } from "../../../general/store/errors/actions"; -import { setUser } from "../../../general/store/user/actions"; -import { useLocalStorage } from "../../../LocalStorageManager"; -import { AuthService } from "../../../services/auth.service"; -import { PaymentsService } from "../../../services/payments.service"; -import { LoginCredentials } from "../../../services/types/command.types"; -import { AuthFormValues } from "../types"; - -interface UseAuthModalProps { - onClose: () => void; - onLogin?: (token: string) => void; - translate: (key: string) => string; -} - -export function useLoginModal({ onClose, onLogin, translate }: UseAuthModalProps) { - const [isLoggingIn, setIsLoggingIn] = useState(false); - const [showTwoFactor, setShowTwoFactor] = useState(false); - const [loginError, setLoginError] = useState(""); - - const storageManager = useLocalStorage(); - const dispatch = useDispatch(); - - const { - register, - formState: { errors }, - handleSubmit, - reset, - watch, - } = useForm({ - mode: "onChange", - }); - - const twoFactorCode = watch("twoFactorCode", ""); - - const resetState = useCallback(() => { - reset(); - setShowTwoFactor(false); - setLoginError(""); - }, [reset]); - - const handleLogin = async (formData: AuthFormValues) => { - setIsLoggingIn(true); - setLoginError(""); - - const { email, password, twoFactorCode: formTwoFactorCode } = formData; - const currentTwoFactorCode = formTwoFactorCode || ""; - - try { - await processLogin(email, password, currentTwoFactorCode); - } catch (err: unknown) { - handleLoginError(err); - } finally { - setIsLoggingIn(false); - } - }; - - const authenticateUser = useCallback( - async (email: string, password: string, twoFactorCode: string) => { - try { - return await AuthService.instance.doLogin(email, password, twoFactorCode); - } catch (err) { - throw new Error(translate("meet.auth.modal.error.invalidCredentials")); - } - }, - [translate] - ); - - const getUserSubscription = useCallback(async () => { - try { - return await PaymentsService.instance.getUserSubscription(); - } catch (err) { - console.error("Error getting user subscription:", err); - return { type: "free" as const }; - } - }, []); - - const saveUserSession = useCallback( - async (credentials: LoginCredentials) => { - try { - storageManager.saveCredentials(credentials.newToken, credentials.mnemonic, credentials.user); - - const subscription = await getUserSubscription(); - - if (subscription) { - storageManager.setSubscription(subscription); - } - - dispatch(loginSuccess(credentials)); - dispatch(setUser(credentials.user)); - onLogin?.(credentials.newToken); - } catch (err) { - storageManager.saveCredentials(credentials.newToken, credentials.mnemonic, credentials.user); - - dispatch(loginSuccess(credentials)); - onLogin?.(credentials.newToken); - } - }, - [storageManager, onLogin, dispatch, getUserSubscription] - ); - - const saveRoomId = useCallback( - (roomID: string) => { - dispatch(setRoomID(roomID)); - }, - [dispatch] - ); - - const processLogin = async (email: string, password: string, twoFactorCode: string) => { - if (!showTwoFactor) { - const is2FANeeded = await AuthService.instance.is2FANeeded(email); - - if (is2FANeeded && !showTwoFactor) { - setShowTwoFactor(true); - return; - } - } - - const loginCredentials = await authenticateUser(email, password, twoFactorCode); - - if (!loginCredentials?.newToken || !loginCredentials?.user) { - throw new Error(translate("meet.auth.modal.error.invalidCredentials")); - } - - // TODO: NEED TO SAVE MEET ROOM TO COMPLETE LOGIN AND REDIRECT TO SCHEDULE MODAL FLOW - // saveRoomId(meetData.room); - - await saveUserSession(loginCredentials); - - onClose(); - }; - - const handleLoginError = useCallback( - (err: unknown) => { - if (err instanceof Error) { - setLoginError(err.message); - } else { - setLoginError(translate("meet.auth.modal.error.genericError")); - } - }, - [translate] - ); - - return { - isLoggingIn, - showTwoFactor, - loginError, - register, - errors, - handleSubmit, - handleLogin, - resetState, - twoFactorCode, - }; -} diff --git a/react/features/base/meet/views/Home/hooks/useSignUp.test.ts b/react/features/base/meet/views/Home/hooks/useSignUp.test.ts deleted file mode 100644 index 04b9ee3e7d6f..000000000000 --- a/react/features/base/meet/views/Home/hooks/useSignUp.test.ts +++ /dev/null @@ -1,286 +0,0 @@ -import { act, renderHook } from "@testing-library/react-hooks"; -import * as bip39 from "bip39"; -import { beforeEach, describe, expect, it, Mock, vi } from "vitest"; -import "../../../__tests__/setup"; -import { CryptoService } from "../../../services/crypto.service"; -import { KeysService } from "../../../services/keys.service"; -import { SdkManager } from "../../../services/sdk-manager.service"; -import { useSignup } from "./useSignUp"; - -vi.mock("bip39"); -vi.mock("../../../services/crypto.service"); -vi.mock("../../../services/keys.service"); -vi.mock("../../../services/sdk-manager.service"); -vi.mock("../../../LocalStorageManager", () => { - const mockInstance = { - getToken: vi.fn(), - }; - return { - default: { - instance: mockInstance, - }, - useLocalStorage: () => ({ - saveCredentials: vi.fn(), - }), - }; -}); - -describe("useSignup", () => { - const mockTranslate = vi.fn((key: string) => `translated-${key}`); - const mockOnClose = vi.fn(); - const mockOnSignup = vi.fn(); - const mockReferrer = "test-referrer"; - - const mockAuthClient = { - register: vi.fn(), - }; - - const mockFormValues = { - email: "test@example.com", - password: "test-password", - confirmPassword: "test-password", - captcha: "test-captcha", - }; - - const mockCryptoService = { - passToHash: vi.fn(), - encryptText: vi.fn(), - encryptTextWithKey: vi.fn(), - }; - - const mockKeysService = { - getKeys: vi.fn(), - }; - - const mockSdkManager = { - getNewAuth: vi.fn(), - }; - - beforeEach(() => { - vi.clearAllMocks(); - - (CryptoService as any).instance = mockCryptoService; - (KeysService as any).instance = mockKeysService; - (SdkManager as any).instance = mockSdkManager; - - mockCryptoService.passToHash.mockReturnValue({ hash: "test-hash", salt: "test-salt" }); - mockCryptoService.encryptText.mockImplementation((text) => `encrypted-${text}`); - mockCryptoService.encryptTextWithKey.mockImplementation((text, key) => `encrypted-with-${key}-${text}`); - - (bip39.generateMnemonic as Mock).mockReturnValue("test-mnemonic"); - - mockKeysService.getKeys.mockResolvedValue({ - publicKey: "public-key", - privateKeyEncrypted: "private-key-encrypted", - revocationCertificate: "revocation-cert", - ecc: { - publicKey: "ecc-public-key", - privateKeyEncrypted: "ecc-private-key-encrypted", - }, - kyber: { - publicKey: null, - privateKeyEncrypted: null, - }, - }); - - mockSdkManager.getNewAuth.mockReturnValue(mockAuthClient); - - mockAuthClient.register.mockResolvedValue({ - token: "test-token", - newToken: "test-new-token", - user: { id: "user-id", email: "test@example.com" }, - uuid: "test-uuid", - }); - }); - - it("should initialize with default state", () => { - const { result } = renderHook(() => - useSignup({ - onClose: mockOnClose, - onSignup: mockOnSignup, - translate: mockTranslate, - }) - ); - - expect(result.current.isSigningUp).toBe(false); - expect(result.current.signupError).toBe(""); - }); - - it("should reset signup state when resetSignupState is called", async () => { - const { result } = renderHook(() => - useSignup({ - onClose: mockOnClose, - onSignup: mockOnSignup, - translate: mockTranslate, - }) - ); - - await act(async () => { - mockAuthClient.register.mockRejectedValueOnce(new Error("Test error")); - await result.current.handleSignup({ - ...mockFormValues, - confirmPassword: "different-password", - }); - }); - - expect(result.current.signupError).not.toBe(""); - - act(() => { - result.current.resetSignupState(); - }); - - expect(result.current.isSigningUp).toBe(false); - expect(result.current.signupError).toBe(""); - }); - - describe("handleSignup", () => { - it("should throw error if passwords do not match", async () => { - const { result } = renderHook(() => - useSignup({ - onClose: mockOnClose, - onSignup: mockOnSignup, - translate: mockTranslate, - }) - ); - - await act(async () => { - await result.current.handleSignup({ - ...mockFormValues, - confirmPassword: "different-password", - }); - }); - - expect(result.current.signupError).toBe("translated-meet.auth.modal.signup.error.passwordsDoNotMatch"); - expect(result.current.isSigningUp).toBe(false); - }); - - it("should validate all aspects of the signup flow except the final onSignup callback", async () => { - const { result } = renderHook(() => - useSignup({ - onClose: mockOnClose, - onSignup: mockOnSignup, - translate: mockTranslate, - referrer: mockReferrer, - }) - ); - - await act(async () => { - await result.current.handleSignup(mockFormValues); - }); - - expect(mockCryptoService.passToHash).toHaveBeenCalledWith({ password: mockFormValues.password }); - expect(mockCryptoService.encryptText).toHaveBeenCalledTimes(2); - expect(bip39.generateMnemonic).toHaveBeenCalledWith(256); - expect(mockKeysService.getKeys).toHaveBeenCalled(); - expect(mockAuthClient.register).toHaveBeenCalled(); - - const registerCall = mockAuthClient.register.mock.calls[0][0]; - expect(registerCall.email).toBe(mockFormValues.email.toLowerCase()); - expect(registerCall.captcha).toBe(mockFormValues.captcha); - expect(registerCall.referrer).toBe(mockReferrer); - expect(registerCall.name).toBe("My"); - expect(registerCall.lastname).toBe("Internxt"); - - expect(result.current.isSigningUp).toBe(false); - }); - - it("should process signup successfully including the onSignup callback", async () => { - let capturedSignupData = null; - - mockOnSignup.mockImplementation((signupData) => { - capturedSignupData = signupData; - }); - - mockAuthClient.register.mockResolvedValue({ - token: "direct-token", - newToken: "direct-new-token", - user: { - id: "direct-id", - email: "direct@example.com", - uuid: "direct-uuid", - }, - }); - const { result } = renderHook(() => - useSignup({ - onClose: mockOnClose, - onSignup: mockOnSignup, - translate: mockTranslate, - referrer: mockReferrer, - }) - ); - - await act(async () => { - await result.current.handleSignup(mockFormValues); - }); - - expect(mockOnSignup).toHaveBeenCalled(); - expect(capturedSignupData).toEqual({ - mnemonic: "test-mnemonic", - token: "direct-token", - newToken: "direct-new-token", - user: { - id: "direct-id", - email: "direct@example.com", - uuid: "direct-uuid", - }, - }); - }); - - it("should handle signup without onSignup callback", async () => { - const { result } = renderHook(() => - useSignup({ - onClose: mockOnClose, - translate: mockTranslate, - }) - ); - - await act(async () => { - await result.current.handleSignup(mockFormValues); - }); - - expect(mockAuthClient.register).toHaveBeenCalled(); - expect(mockOnSignup).not.toHaveBeenCalled(); - expect(mockOnClose).toHaveBeenCalled(); - }); - - it("should handle API errors during signup", async () => { - mockAuthClient.register.mockRejectedValue(new Error("API error")); - - const { result } = renderHook(() => - useSignup({ - onClose: mockOnClose, - onSignup: mockOnSignup, - translate: mockTranslate, - }) - ); - - await act(async () => { - await result.current.handleSignup(mockFormValues); - }); - - expect(result.current.signupError).toBe("API error"); - expect(result.current.isSigningUp).toBe(false); - expect(mockOnSignup).not.toHaveBeenCalled(); - expect(mockOnClose).not.toHaveBeenCalled(); - }); - - it("should handle errors without message during signup", async () => { - mockAuthClient.register.mockRejectedValue({}); - - const { result } = renderHook(() => - useSignup({ - onClose: mockOnClose, - onSignup: mockOnSignup, - translate: mockTranslate, - }) - ); - - await act(async () => { - await result.current.handleSignup(mockFormValues); - }); - - expect(result.current.signupError).toBe("translated-meet.auth.modal.signup.error.signupFailed"); - expect(result.current.isSigningUp).toBe(false); - }); - }); -}); diff --git a/react/features/base/meet/views/Home/hooks/useSignUp.ts b/react/features/base/meet/views/Home/hooks/useSignUp.ts deleted file mode 100644 index 670fb37c2b55..000000000000 --- a/react/features/base/meet/views/Home/hooks/useSignUp.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { RegisterDetails } from "@internxt/sdk"; -import { UserSettings } from "@internxt/sdk/dist/shared/types/userSettings"; -import * as bip39 from "bip39"; -import { useState } from "react"; -import { useLocalStorage } from "../../../LocalStorageManager"; -import { CryptoService } from "../../../services/crypto.service"; -import { KeysService } from "../../../services/keys.service"; -import { SdkManager } from "../../../services/sdk-manager.service"; -import { LoginCredentials } from "../../../services/types/command.types"; -import { IFormValues } from "../types"; - -interface useSignupProps { - onClose: () => void; - onSignup?: (signupData: LoginCredentials) => void; - translate: (key: string) => string; - referrer?: string; -} - -export interface RegisterResponse { - token: string; - newToken: string; - user: UserSettings; - uuid: string; -} - -export const useSignup = ({ onClose, onSignup, translate, referrer }: useSignupProps) => { - const [isSigningUp, setIsSigningUp] = useState(false); - const [signupError, setSignupError] = useState(""); - const storageManager = useLocalStorage(); - - const resetSignupState = () => { - setIsSigningUp(false); - setSignupError(""); - }; - - const handleAutoLogin = async (registerData: RegisterResponse, mnemonic: string) => { - const { token, newToken, user } = registerData; - - storageManager.saveCredentials(newToken, mnemonic, user); - - onSignup?.({ - mnemonic, - token, - newToken, - user, - }); - }; - - const handleSignup = async (data: IFormValues) => { - try { - setIsSigningUp(true); - setSignupError(""); - - const { email, password, confirmPassword } = data; - - if (password !== confirmPassword) { - throw new Error(translate("meet.auth.modal.signup.error.passwordsDoNotMatch")); - } - - const hashObj = CryptoService.instance.passToHash({ password }); - const encPass = CryptoService.instance.encryptText(hashObj.hash); - const encSalt = CryptoService.instance.encryptText(hashObj.salt); - - const mnemonic = bip39.generateMnemonic(256); - const encMnemonic = CryptoService.instance.encryptTextWithKey(mnemonic, password); - - const keys = await KeysService.instance.getKeys(password); - const captcha = data.captcha; - - const registerDetails: RegisterDetails = { - name: "My", - lastname: "Internxt", - email: email.toLowerCase(), - password: encPass, - salt: encSalt, - mnemonic: encMnemonic, - keys: keys, - captcha: captcha, - referrer: referrer, - }; - - const authClient = SdkManager.instance.getNewAuth(); - - const registerUserData = await authClient.register(registerDetails); - - const fullResponse: RegisterResponse = { - ...registerUserData, - // CAST DONE BECAUSE THE SDK TYPE IS NOT CORRECT - } as unknown as RegisterResponse; - - await handleAutoLogin(fullResponse, mnemonic); - - onClose(); - } catch (error: any) { - setSignupError(error.message ?? translate("meet.auth.modal.signup.error.signupFailed")); - } finally { - setIsSigningUp(false); - } - }; - - return { - isSigningUp, - signupError, - handleSignup, - resetSignupState, - }; -}; diff --git a/react/test/setup.ts b/react/test/setup.ts index 791172b82d27..4b3ea7d317fc 100644 --- a/react/test/setup.ts +++ b/react/test/setup.ts @@ -1,35 +1,6 @@ import "@testing-library/jest-dom"; import { vi } from "vitest"; -vi.mock("@openpgp/web-stream-tools", () => ({ - concatUint8Array: vi.fn((arr) => arr || new Uint8Array()), - concat: vi.fn(async () => new Uint8Array()), - stream: { - transform: vi.fn((data) => data), - readToEnd: vi.fn(async () => new Uint8Array()), - slice: vi.fn((stream) => stream), - }, - Reader: vi.fn(), - Writer: vi.fn(), -})); - -vi.mock('openpgp', () => ({ - default: { - readKey: vi.fn(), - readPrivateKey: vi.fn(), - readMessage: vi.fn(), - encrypt: vi.fn(), - decrypt: vi.fn(), - generateKey: vi.fn(), - }, - readKey: vi.fn(), - readPrivateKey: vi.fn(), - readMessage: vi.fn(), - encrypt: vi.fn(), - decrypt: vi.fn(), - generateKey: vi.fn(), -})); - const localStorageMock = (() => { let store: Record = {}; diff --git a/webpack.config.js b/webpack.config.js index 89972b80d280..4d824b981fa4 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -358,9 +358,6 @@ module.exports = (_env, argv) => { "DRIVE_NEW_API_URL", "PAYMENTS_API_URL", "MEET_API_URL", - "CRYPTO_SECRET", - "MAGIC_IV", - "MAGIC_SALT", ]; const env = {}; keys.forEach((key) => { diff --git a/yarn.lock b/yarn.lock index 8fa6bb5ad78e..54270a15bcaf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6450,16 +6450,6 @@ asn1.js@^4.10.1: inherits "^2.0.1" minimalistic-assert "^1.0.0" -asn1.js@^5.0.0: - version "5.4.1" - resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-5.4.1.tgz#11a980b84ebb91781ce35b0fdc2ee294e3783f07" - integrity sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA== - dependencies: - bn.js "^4.0.0" - inherits "^2.0.1" - minimalistic-assert "^1.0.0" - safer-buffer "^2.1.0" - assertion-error@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-2.0.1.tgz#f641a196b335690b1070bf00b6e7593fec190bf7" @@ -9986,11 +9976,6 @@ hash-base@~3.0.4: inherits "^2.0.4" safe-buffer "^5.2.1" -hash-wasm@=4.11.0: - version "4.11.0" - resolved "https://registry.yarnpkg.com/hash-wasm/-/hash-wasm-4.11.0.tgz#7d1479b114c82e48498fdb1d2462a687d00386d5" - integrity sha512-HVusNXlVqHe0fzIzdQOGolnFN6mX/fqcrSAOcTBXdvzrXVHwTz11vXeKRmkR5gTuwVpvHZEIyKoePDvuAR+XwQ== - hash-wasm@^4.12.0: version "4.12.0" resolved "https://registry.yarnpkg.com/hash-wasm/-/hash-wasm-4.12.0.tgz#f9f1a9f9121e027a9acbf6db5d59452ace1ef9bb" @@ -12839,13 +12824,6 @@ opener@^1.5.2: resolved "https://registry.yarnpkg.com/opener/-/opener-1.5.2.tgz#5d37e1f35077b9dcac4301372271afdeb2a13598" integrity sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A== -openpgp@^5.11.1: - version "5.11.3" - resolved "https://registry.yarnpkg.com/openpgp/-/openpgp-5.11.3.tgz#a2532aa973f1f6413556eaf328b97a6955b1d8a3" - integrity sha512-jXOPfIteBUQ2zSmRG4+Y6PNntIIDEAvoM/lOYCnvpXAByJEruzrHQZWE/0CGOKHbubwUuty2HoPHsqBzyKHOpA== - dependencies: - asn1.js "^5.0.0" - optional-require@1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/optional-require/-/optional-require-1.0.3.tgz#275b8e9df1dc6a17ad155369c2422a440f89cb07" @@ -14583,7 +14561,7 @@ safe-regex2@^5.0.0: dependencies: ret "~0.5.0" -"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0", safer-buffer@^2.1.0: +"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0": version "2.1.2" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== From 51dc48315298ebfae12ca0eeab7ba813d19cd804 Mon Sep 17 00:00:00 2001 From: tamarafinogina Date: Thu, 7 May 2026 18:16:53 +0200 Subject: [PATCH 08/11] switch to keepAlive version of sdk --- package.json | 2 +- react/features/base/connection/actions.web.ts | 2 +- .../base/meet/views/Conference/Conference.tsx | 30 ++----- yarn.lock | 86 ++++++++++++------- 4 files changed, 64 insertions(+), 56 deletions(-) diff --git a/package.json b/package.json index af3ed1efd5e1..7d4d6937cad9 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "@internxt/css-config": "^1.1.0", "@internxt/eslint-config-internxt": "^2.0.1", "@internxt/lib": "^1.4.1", - "@internxt/sdk": "1.15.6", + "@internxt/sdk": "1.15.14", "@internxt/ui": "0.1.1", "@jitsi/excalidraw": "https://github.com/jitsi/excalidraw/releases/download/v0.0.19/jitsi-excalidraw-0.0.19.tgz", "@jitsi/js-utils": "2.6.7", diff --git a/react/features/base/connection/actions.web.ts b/react/features/base/connection/actions.web.ts index 0f1a3649d38c..2cd1e73e367a 100644 --- a/react/features/base/connection/actions.web.ts +++ b/react/features/base/connection/actions.web.ts @@ -21,7 +21,7 @@ import logger from './logger'; * @param {string} roomId - The room ID to leave * @returns {Promise} */ -async function leaveCallWithUserIdentification(roomId: string): Promise { +export async function leaveCallWithUserIdentification(roomId: string): Promise { const user = LocalStorageManager.instance.getUser(); let payload = undefined; if (!user){ diff --git a/react/features/base/meet/views/Conference/Conference.tsx b/react/features/base/meet/views/Conference/Conference.tsx index ec64cef01751..ccfdc0de4795 100644 --- a/react/features/base/meet/views/Conference/Conference.tsx +++ b/react/features/base/meet/views/Conference/Conference.tsx @@ -11,7 +11,7 @@ import { AbstractConference, abstractMapStateToProps } from "../../../../confere import { maybeShowSuboptimalExperienceNotification } from "../../../../conference/functions.web"; import { toggleToolboxVisible } from "../../../../toolbox/actions.any"; import { fullScreenChanged, showToolbox } from "../../../../toolbox/actions.web"; -import { hangup } from "../../../connection/actions.web"; +import { hangup, leaveCallWithUserIdentification } from "../../../connection/actions.web"; import { translate } from "../../../i18n/functions"; import { setColorAlpha } from "../../../util/helpers"; import { Mode } from "./components/Header"; @@ -20,8 +20,6 @@ import { init } from "../../../../conference/actions.web"; import CreateConference from "./containers/CreateConference"; import JoinConference from "./containers/JoinConference"; import { appNavigate } from "../../../../app/actions.web"; -import { LocalStorageManager } from "../../LocalStorageManager"; -import { ConfigService } from "../../services/config.service"; /** * DOM events for when full screen mode has changed. Different browsers need @@ -218,30 +216,12 @@ class Conference extends AbstractConference { * @returns {string} */ _handlePageHide = (): void => { - console.log("[RELOAD]: Page is being hidden, sending leave call request"); - const callId = this.props.roomId; - const token = LocalStorageManager.instance.getNewToken(); - let body = ''; - if(!token) { - const anonymousUserId = LocalStorageManager.instance.getAnonymousUUID(); - body = JSON.stringify({ userId: anonymousUserId }); + if (callId) { + console.log("[RELOAD]: _handlePageHide calls leaveCall with callID:", callId); + leaveCallWithUserIdentification(callId); } - - const MEET_API_URL = ConfigService.instance.get("MEET_API_URL"); - - fetch(`${MEET_API_URL}/call/${callId}/users/leave`, { - method: "POST", - keepalive: true, - headers: { - "Content-Type": "application/json; charset=utf-8", - Accept: "application/json, text/plain, */*", - Authorization: `Bearer ${token}`, - "internxt-version": "0.0.1", - "internxt-client": "internxt-meet", - }, - body, - }); + console.log("[RELOAD]: _handlePageHide done"); }; /** diff --git a/yarn.lock b/yarn.lock index 8fa6bb5ad78e..c5de6dfc83c6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2532,13 +2532,13 @@ dependencies: uuid "^11.1.0" -"@internxt/sdk@1.15.6": - version "1.15.6" - resolved "https://registry.yarnpkg.com/@internxt/sdk/-/sdk-1.15.6.tgz#f895cf12b160fd23c386e6d4d007ec29e51d9158" - integrity sha512-DRGlj2XArmBNa9wM55MbR9E4scNPDer+emtrugG1MSZvP/YOSv5XOes5j/df5ZUgMyRs9G0O7hwCtjn9XvW9YA== +"@internxt/sdk@1.15.14": + version "1.15.14" + resolved "https://registry.yarnpkg.com/@internxt/sdk/-/sdk-1.15.14.tgz#f9f6f35832dea50fc6642fadfd61c974ed3f1a03" + integrity sha512-QGdQg8jtR3FZCwowGoIXniN7DConu2duUcBDHsLDM5kJ6hymtXwejUzqdVg4DDSatTvvHD2YobEINgh6ND6ysw== dependencies: - axios "1.13.6" - internxt-crypto "0.0.14" + axios "1.15.2" + internxt-crypto "1.0.2" "@internxt/ui@0.1.1": version "0.1.1" @@ -2989,11 +2989,23 @@ dependencies: eslint-scope "5.1.1" -"@noble/ciphers@^2.0.1", "@noble/ciphers@^2.1.1": +"@noble/ciphers@^2.0.1": version "2.1.1" resolved "https://registry.yarnpkg.com/@noble/ciphers/-/ciphers-2.1.1.tgz#c8c74fcda8c3d1f88797d0ecda24f9fc8b92b052" integrity sha512-bysYuiVfhxNJuldNXlFEitTVdNnYUc+XNJZd7Qm2a5j1vZHgY+fazadNFWFaMK/2vye0JVlxV3gHmC0WDfAOQw== +"@noble/ciphers@^2.1.1": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@noble/ciphers/-/ciphers-2.2.0.tgz#84fb45ac9332925d643b80f89ceb0ea2f21dba95" + integrity sha512-Z6pjIZ/8IJcCGzb2S/0Px5J81yij85xASuk1teLNeg75bfT07MV3a/O2Mtn1I2se43k3lkVEcFaR10N4cgQcZA== + +"@noble/curves@^2.0.1": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-2.2.0.tgz#981be3aadc3bbfbcdb245e78cc97aa6f759246c2" + integrity sha512-T/BoHgFXirb0ENSPBquzX0rcjXeM6Lo892a2jlYJkqk83LqZx0l1Of7DzlKJ6jkpvMrkHSnAcgb5JegL8SeIkQ== + dependencies: + "@noble/hashes" "2.2.0" + "@noble/curves@~2.0.0": version "2.0.1" resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-2.0.1.tgz#64ba8bd5e8564a02942655602515646df1cdb3ad" @@ -3006,6 +3018,11 @@ resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-2.0.1.tgz#fc1a928061d1232b0a52bb754393c37a5216c89e" integrity sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw== +"@noble/hashes@2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-2.2.0.tgz#22da1d16a469954fce877055d559900a6c73b63b" + integrity sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg== + "@noble/hashes@^1.2.0": version "1.8.0" resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.8.0.tgz#cee43d801fcef9644b11b8194857695acd5f815a" @@ -4461,18 +4478,18 @@ resolved "https://registry.yarnpkg.com/@sayem314/react-native-keep-awake/-/react-native-keep-awake-1.3.1.tgz#59cd52923ba3000adfec6918a3a935ae335575a7" integrity sha512-gAqLCVQ2SgrMki9MZJzQiYn9EjOGDYze+dYKs7s7T4qfRrUxCK8Pe50mE0Y/WQqzaYY6uzdjTHzmWhACiEy5Zw== -"@scure/base@2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@scure/base/-/base-2.0.0.tgz#ba6371fddf92c2727e88ad6ab485db6e624f9a98" - integrity sha512-3E1kpuZginKkek01ovG8krQ0Z44E3DHPjc5S2rjJw9lZn3KSQOs8S7wqikF/AH7iRanHypj85uGyxk0XAyC37w== +"@scure/base@2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@scure/base/-/base-2.2.0.tgz#1311378ed247df6d58f8eb8941921965e97e5747" + integrity sha512-b8XEupJibegiXV+tDUseI8oLQc8ei3d/4Jkb2RpbHh3MfE054ov3uIz2dhFkB3FI8iwYkEh0gGCApkrYggkPNg== "@scure/bip39@^2.0.1": - version "2.0.1" - resolved "https://registry.yarnpkg.com/@scure/bip39/-/bip39-2.0.1.tgz#47a6dc15e04faf200041239d46ae3bb7c3c96add" - integrity sha512-PsxdFj/d2AcJcZDX1FXN3dDgitDDTmwf78rKZq1a6c1P1Nan1X/Sxc7667zU3U+AN60g7SxxP0YCVw2H/hBycg== + version "2.2.0" + resolved "https://registry.yarnpkg.com/@scure/bip39/-/bip39-2.2.0.tgz#7a3da0564aa2af919280a12b892d4b57e1bf30be" + integrity sha512-T/Bj/YvYMNkIPq6EENO6/rcs2e7qTNuyoUXf0KBFDmp0ZDu0H2X4Lq6yC3i0c8PcWkov5EbW+yQZZbdMmk154A== dependencies: - "@noble/hashes" "2.0.1" - "@scure/base" "2.0.0" + "@noble/hashes" "2.2.0" + "@scure/base" "2.2.0" "@sec-ant/readable-stream@^0.4.1": version "0.4.1" @@ -6542,14 +6559,14 @@ available-typed-arrays@^1.0.7: dependencies: possible-typed-array-names "^1.0.0" -axios@1.13.6: - version "1.13.6" - resolved "https://registry.yarnpkg.com/axios/-/axios-1.13.6.tgz#c3f92da917dc209a15dd29936d20d5089b6b6c98" - integrity sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ== +axios@1.15.2: + version "1.15.2" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.15.2.tgz#eb8fb6d30349abace6ade5b4cb4d9e8a0dc23e5b" + integrity sha512-wLrXxPtcrPTsNlJmKjkPnNPK2Ihe0hn0wGSaTEiHRPxwjvJwT3hKmXF4dpqxmPO9SoNb2FsYXj/xEo0gHN+D5A== dependencies: follow-redirects "^1.15.11" form-data "^4.0.5" - proxy-from-env "^1.1.0" + proxy-from-env "^2.1.0" b4a@^1.6.4: version "1.7.3" @@ -9589,11 +9606,16 @@ focus-visible@5.1.0: resolved "https://registry.yarnpkg.com/focus-visible/-/focus-visible-5.1.0.tgz#4b9d40143b865f53eafbd93ca66672b3bf9e7b6a" integrity sha512-nPer0rjtzdZ7csVIu233P2cUm/ks/4aVSI+5KUkYrYpgA7ujgC3p6J7FtFU+AIMWwnwYQOB/yeiOITxFeYIXiw== -follow-redirects@^1.0.0, follow-redirects@^1.15.11: +follow-redirects@^1.0.0: version "1.15.11" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.11.tgz#777d73d72a92f8ec4d2e410eb47352a56b8e8340" integrity sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ== +follow-redirects@^1.15.11: + version "1.16.0" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.16.0.tgz#28474a159d3b9d11ef62050a14ed60e4df6d61bc" + integrity sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw== + for-each@^0.3.3, for-each@^0.3.5: version "0.3.5" resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.5.tgz#d650688027826920feeb0af747ee7b9421a41d47" @@ -10414,12 +10436,13 @@ internal-slot@^1.1.0: hasown "^2.0.2" side-channel "^1.1.0" -internxt-crypto@0.0.14: - version "0.0.14" - resolved "https://registry.yarnpkg.com/internxt-crypto/-/internxt-crypto-0.0.14.tgz#1290b2a70190c23d25b83483de8200d9eafae00f" - integrity sha512-gIvqgou0r86kSk6x2t6pxAh9dJiob/sQ1Y3TdGnAF4Qq2RD++4Aq1b6NY2UqfUYV4vPhWsd2BkFS71jAyVrXpA== +internxt-crypto@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/internxt-crypto/-/internxt-crypto-1.0.2.tgz#983fe991dfbb00a453e93070bb34049a88a94707" + integrity sha512-F9PuXci0eU1wlgDwqEbGR7hVDNS0MX8VNh/W+pdpR4ZsEsjRDBrOD2g1DvViR2woCxPiu1AW9Wwekpw2YVKfnA== dependencies: "@noble/ciphers" "^2.1.1" + "@noble/curves" "^2.0.1" "@noble/hashes" "^2.0.1" "@noble/post-quantum" "^0.5.2" "@scure/bip39" "^2.0.1" @@ -13505,6 +13528,11 @@ proxy-from-env@^1.1.0: resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== +proxy-from-env@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-2.1.0.tgz#a7487568adad577cfaaa7e88c49cab3ab3081aba" + integrity sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA== + public-encrypt@^4.0.3: version "4.0.3" resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.3.tgz#4fcc9d77a07e48ba7527e7cbe0de33d0701331e0" @@ -16193,9 +16221,9 @@ uuid@^11.1.0: integrity sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A== uuid@^13.0.0: - version "13.0.0" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-13.0.0.tgz#263dc341b19b4d755eb8fe36b78d95a6b65707e8" - integrity sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w== + version "13.0.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-13.0.2.tgz#41bc9c07b12f665089c205f6507976adbdf84ff8" + integrity sha512-vzi9uRZ926x4XV73S/4qQaTwPXM2JBj6/6lI/byHH1jOpCzb0zDbfytgA9LcN/hzb2l7WQSQnxITOVx5un/wGw== uuid@^9.0.0: version "9.0.1" From 3f25c18f89cc3fd76efe3785fd264074b2156092 Mon Sep 17 00:00:00 2001 From: tamarafinogina Date: Thu, 7 May 2026 18:18:19 +0200 Subject: [PATCH 09/11] use session storage for anonymous ID --- react/features/base/connection/actions.any.ts | 4 +- react/features/base/connection/actions.web.ts | 3 +- .../features/base/meet/LocalStorageManager.ts | 38 ------------------- .../base/meet/SessionStorageManager.ts | 33 ++++++++++++++++ 4 files changed, 37 insertions(+), 41 deletions(-) create mode 100644 react/features/base/meet/SessionStorageManager.ts diff --git a/react/features/base/connection/actions.any.ts b/react/features/base/connection/actions.any.ts index 6101fbeee3c9..30a35775d6da 100644 --- a/react/features/base/connection/actions.any.ts +++ b/react/features/base/connection/actions.any.ts @@ -15,7 +15,6 @@ import { } from '../util/uri'; import { setJoinRoomError } from "../meet/general/store/errors/actions"; -import { LocalStorageManager } from "../meet/LocalStorageManager"; import MeetingService from "../meet/services/meeting.service"; import { clearNewMeetingFlowSession } from "../meet/services/sessionStorage.service"; import { @@ -33,6 +32,7 @@ import logger from "./logger"; import { get8x8Options } from "./options8x8"; import { ConnectionFailedError, IIceServers } from "./types"; import { ConfigService } from '../meet/services/config.service'; +import { SessionStorageManager } from '../meet/SessionStorageManager'; /** * The options that will be passed to the JitsiConnection instance. @@ -246,7 +246,7 @@ export function _connectInternal({ let userUUID: string | undefined; if (isAnonymous) { - userUUID = LocalStorageManager.instance.getOrCreateAnonymousUUID(); + userUUID = SessionStorageManager.instance.getOrCreateAnonymousUUID(); } const { token: jwt, appId } = await MeetingService.instance.joinCall(room, { name: displayName ?? name ?? "", diff --git a/react/features/base/connection/actions.web.ts b/react/features/base/connection/actions.web.ts index 2cd1e73e367a..b35376d4e6c2 100644 --- a/react/features/base/connection/actions.web.ts +++ b/react/features/base/connection/actions.web.ts @@ -14,6 +14,7 @@ import { LocalStorageManager } from "../meet/LocalStorageManager"; import MeetingService from "../meet/services/meeting.service"; import { _connectInternal } from "./actions.any"; import logger from './logger'; +import { SessionStorageManager } from '../meet/SessionStorageManager'; /** * Helper function to leave a call with proper user identification (authenticated or anonymous) @@ -25,7 +26,7 @@ export async function leaveCallWithUserIdentification(roomId: string): Promise(LocalStorageManager.KEYS.ANONYMOUS_USER_UUID); - - if (!uuid) { - uuid = v4(); - this.set(LocalStorageManager.KEYS.ANONYMOUS_USER_UUID, uuid); - } - - return uuid; - } - - /** - * Gets the anonymous user UUID - */ - public getAnonymousUUID(): string | null | undefined { - return this.get(LocalStorageManager.KEYS.ANONYMOUS_USER_UUID); - } - - /** - * Sets the anonymous user UUID - */ - public setAnonymousUUID(uuid: string): void { - this.set(LocalStorageManager.KEYS.ANONYMOUS_USER_UUID, uuid); - } - - /** - * Removes the anonymous user UUID - */ - public removeAnonymousUUID(): void { - this.remove(LocalStorageManager.KEYS.ANONYMOUS_USER_UUID); - } /** * Saves the session credentials @@ -232,7 +195,6 @@ export class LocalStorageManager { this.remove(LocalStorageManager.KEYS.MNEMONIC); this.remove(LocalStorageManager.KEYS.USER); this.remove(LocalStorageManager.KEYS.SUBSCRIPTION); - this.remove(LocalStorageManager.KEYS.ANONYMOUS_USER_UUID); } public clearStorage(): void { diff --git a/react/features/base/meet/SessionStorageManager.ts b/react/features/base/meet/SessionStorageManager.ts new file mode 100644 index 000000000000..34ca68f69897 --- /dev/null +++ b/react/features/base/meet/SessionStorageManager.ts @@ -0,0 +1,33 @@ +import { v4 } from "uuid"; + +const ANON_UUID_KEY = "xAnonymousUserUUID"; + +export class SessionStorageManager { + private static _instance: SessionStorageManager; + + private constructor() {} + + public static get instance(): SessionStorageManager { + if (!SessionStorageManager._instance) { + SessionStorageManager._instance = new SessionStorageManager(); + } + return SessionStorageManager._instance; + } + + public getOrCreateAnonymousUUID(): string { + let uuid = this.getAnonymousUUID(); + if (!uuid) { + uuid = v4(); + sessionStorage.setItem(ANON_UUID_KEY, uuid); + } + + return uuid; + } + + public getAnonymousUUID(): string | null { + return sessionStorage.getItem(ANON_UUID_KEY); + } + +} + +export default SessionStorageManager.instance; \ No newline at end of file From fec919bf3081c0ad113f52c5ec0685eebc9b04d8 Mon Sep 17 00:00:00 2001 From: tamarafinogina Date: Fri, 8 May 2026 09:53:05 +0200 Subject: [PATCH 10/11] set notifyOnConferenceDestruction=true in config --- config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.js b/config.js index 41f1ab78feb8..cf4a803e2d24 100644 --- a/config.js +++ b/config.js @@ -789,7 +789,7 @@ var config = { // enableCalendarIntegration: false, // Whether to notify when the conference is terminated because it was destroyed. - // notifyOnConferenceDestruction: true, + notifyOnConferenceDestruction: true, // The client id for the google APIs used for the calendar integration, youtube livestreaming, etc. // googleApiApplicationClientID: '', From b02c8a89a97d9184cf667c9b95140b5a68607f17 Mon Sep 17 00:00:00 2001 From: tamarafinogina Date: Fri, 8 May 2026 11:09:48 +0200 Subject: [PATCH 11/11] add tests for sonar --- .../base/meet/SessionStorageManager.test.ts | 49 +++++++++++++++++++ .../base/meet/SessionStorageManager.ts | 2 +- 2 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 react/features/base/meet/SessionStorageManager.test.ts diff --git a/react/features/base/meet/SessionStorageManager.test.ts b/react/features/base/meet/SessionStorageManager.test.ts new file mode 100644 index 000000000000..bbfb5d8ed4ec --- /dev/null +++ b/react/features/base/meet/SessionStorageManager.test.ts @@ -0,0 +1,49 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { SessionStorageManager, ANON_UUID_KEY } from "./SessionStorageManager"; +import { v4 } from "uuid"; + +const MOCK_UUID = "mock-uuid-1234"; +vi.mock("uuid", () => ({ v4: vi.fn(() => MOCK_UUID) })); + +describe("SessionStorageManager tests", () => { + beforeEach(() => { + sessionStorage.clear(); + vi.clearAllMocks(); + }); + + it("returns the same instance on repeated access", () => { + const a = SessionStorageManager.instance; + const b = SessionStorageManager.instance; + expect(a).toBe(b); + }); + + it("returns null when no anonymous UUID is stored", () => { + expect(SessionStorageManager.instance.getAnonymousUUID()).toBeNull(); + }); + + it("returns the stored anonymous UUID when one exists", () => { + sessionStorage.setItem(ANON_UUID_KEY, "existing-uuid"); + expect(SessionStorageManager.instance.getAnonymousUUID()).toBe("existing-uuid"); + }); + + it("generates and stores a UUID when none exists", () => { + const result = SessionStorageManager.instance.getOrCreateAnonymousUUID(); + expect(result).toBe(MOCK_UUID); + expect(sessionStorage.getItem(ANON_UUID_KEY)).toBe(MOCK_UUID); + }); + + it("returns existing UUID without generating a new one", async () => { + sessionStorage.setItem(ANON_UUID_KEY, "pre-existing-uuid"); + + const result = SessionStorageManager.instance.getOrCreateAnonymousUUID(); + + expect(result).toBe("pre-existing-uuid"); + expect(v4).not.toHaveBeenCalled(); + }); + + it("does not bleed into localStorage", () => { + SessionStorageManager.instance.getOrCreateAnonymousUUID(); + expect(localStorage.getItem(ANON_UUID_KEY)).toBeNull(); + }); + +}); \ No newline at end of file diff --git a/react/features/base/meet/SessionStorageManager.ts b/react/features/base/meet/SessionStorageManager.ts index 34ca68f69897..7f9d02c0c1d2 100644 --- a/react/features/base/meet/SessionStorageManager.ts +++ b/react/features/base/meet/SessionStorageManager.ts @@ -1,6 +1,6 @@ import { v4 } from "uuid"; -const ANON_UUID_KEY = "xAnonymousUserUUID"; +export const ANON_UUID_KEY = "xAnonymousUserUUID"; export class SessionStorageManager { private static _instance: SessionStorageManager;