From 8e9bc0fe7f81af7ec54d911ec810e263511d6925 Mon Sep 17 00:00:00 2001 From: "marc.sirisak" Date: Thu, 19 Sep 2024 09:44:30 +0200 Subject: [PATCH 1/5] feat(sso): add email domain precheck sso flow --- .../matrix-react-sdk/src/Views.ts | 3 + .../src/components/structures/MatrixChat.tsx | 39 ++++- .../src/dispatcher/actions.ts | 5 + .../tchap_translations.json | 32 ++++ patches/subtree-modifications.json | 6 + res/css/views/sso/TchapSSO.pcss | 44 +++++ res/welcome.html | 83 ++++++++- res/welcome/images/proconnect.svg | 22 +++ .../views/sso/EmailVerificationPage.tsx | 159 ++++++++++++++++++ 9 files changed, 389 insertions(+), 4 deletions(-) create mode 100644 res/css/views/sso/TchapSSO.pcss create mode 100644 res/welcome/images/proconnect.svg create mode 100644 src/tchap/components/views/sso/EmailVerificationPage.tsx diff --git a/linked-dependencies/matrix-react-sdk/src/Views.ts b/linked-dependencies/matrix-react-sdk/src/Views.ts index 4c7f002d50..a9c01f646f 100644 --- a/linked-dependencies/matrix-react-sdk/src/Views.ts +++ b/linked-dependencies/matrix-react-sdk/src/Views.ts @@ -54,6 +54,9 @@ enum Views { // Another instance of the application has started up. We just show an error page. LOCK_STOLEN, + + // :TCHAP: screen before launching sso + EMAIL_PRECHECK_SSO } export default Views; diff --git a/linked-dependencies/matrix-react-sdk/src/components/structures/MatrixChat.tsx b/linked-dependencies/matrix-react-sdk/src/components/structures/MatrixChat.tsx index c1d1396ce2..42e78af6aa 100644 --- a/linked-dependencies/matrix-react-sdk/src/components/structures/MatrixChat.tsx +++ b/linked-dependencies/matrix-react-sdk/src/components/structures/MatrixChat.tsx @@ -143,9 +143,11 @@ import { checkSessionLockFree, getSessionLock } from "../../utils/SessionLock"; import { SessionLockStolenView } from "./auth/SessionLockStolenView"; import { ConfirmSessionLockTheftView } from "./auth/ConfirmSessionLockTheftView"; import { LoginSplashView } from "./auth/LoginSplashView"; -import TchapUrls from "../../../../../src/tchap/util/TchapUrls"; // :TCHAP: activate-cross-signing-and-secure-storage-react import { cleanUpDraftsIfRequired } from "../../DraftCleaner"; +import TchapUrls from "../../../../../src/tchap/util/TchapUrls"; // :TCHAP: activate-cross-signing-and-secure-storage-react +import EmailVerificationPage from "../../../../../src/tchap/components/views/sso/EmailVerificationPage"; // :TCHAP: sso-agentconnect-flow + // legacy export export { default as Views } from "../../Views"; @@ -946,6 +948,15 @@ export default class MatrixChat extends React.PureComponent { true, ); break; + // :TCHAP: sso-agentconnect-flow + case Action.EmailPrecheckSSO: + if (Lifecycle.isSoftLogout()) { + this.onSoftLogout(); + break; + } + this.viewEmailPrecheckSSO(); + break; + // end :TCHAP: } }; @@ -1104,6 +1115,17 @@ export default class MatrixChat extends React.PureComponent { this.themeWatcher.recheck(); } + // :TCHAP: sso-agentconnect-flow + private viewEmailPrecheckSSO() { + this.setStateForNewView({ + view: Views.EMAIL_PRECHECK_SSO + }); + this.notifyNewScreen("email-precheck-sso"); + ThemeController.isLogin = true; + this.themeWatcher.recheck(); + } + // end :TCHAP: + private viewHome(justRegistered = false): void { // The home page requires the "logged in" view, so we'll set that. this.setStateForNewView({ @@ -1875,6 +1897,13 @@ export default class MatrixChat extends React.PureComponent { userId: userId, subAction: params?.action, }); + // :TCHAP: sso-agentconnect-flow + } else if (screen = "email-precheck-sso") { + dis.dispatch({ + action: "email_precheck_sso", + params + }); + // end :TCHAP: } else { logger.info(`Ignoring showScreen for '${screen}'`); } @@ -2017,7 +2046,9 @@ export default class MatrixChat extends React.PureComponent { if ( initialScreenAfterLogin && // XXX: workaround for https://github.com/vector-im/element-web/issues/11643 causing a login-loop - !["welcome", "login", "register", "start_sso", "start_cas"].includes(initialScreenAfterLogin.screen) + // :TCHAP: sso-agentconnect-flow !["welcome", "login", "register", "start_sso", "start_cas"].includes(initialScreenAfterLogin.screen) + !["welcome", "login", "register", "start_sso", "start_cas", "email-precheck-sso"].includes(initialScreenAfterLogin.screen) + // end :TCHAP: ) { fragmentAfterLogin = `/${initialScreenAfterLogin.screen}`; } @@ -2137,6 +2168,10 @@ export default class MatrixChat extends React.PureComponent { view = => this.onShowPostLoginScreen(useCase)} />; } else if (this.state.view === Views.LOCK_STOLEN) { view = ; + // :TCHAP: sso-agentconnect-flow + } else if (this.state.view === Views.EMAIL_PRECHECK_SSO) { + view = ; + // end :TCHAP: } else { logger.error(`Unknown view ${this.state.view}`); return null; diff --git a/linked-dependencies/matrix-react-sdk/src/dispatcher/actions.ts b/linked-dependencies/matrix-react-sdk/src/dispatcher/actions.ts index 7cff1cc859..267b934e97 100644 --- a/linked-dependencies/matrix-react-sdk/src/dispatcher/actions.ts +++ b/linked-dependencies/matrix-react-sdk/src/dispatcher/actions.ts @@ -393,4 +393,9 @@ export enum Action { * Opens right panel room summary and focuses the search input */ FocusMessageSearch = "focus_search", + + /** + * :TCHAP: Open new page to check email instance before launching SSO + */ + EmailPrecheckSSO = "email_precheck_sso" } diff --git a/modules/tchap-translations/tchap_translations.json b/modules/tchap-translations/tchap_translations.json index 8078e1b53b..072864f280 100644 --- a/modules/tchap-translations/tchap_translations.json +++ b/modules/tchap-translations/tchap_translations.json @@ -837,5 +837,37 @@ "incompatible_browser|continue": { "en": "Continue anyway", "fr": "Continuer tout de même" + }, + "auth|sso|sign_in_password_instead": { + "en": "Login with password", + "fr": "Se connecter par mot de passe" + }, + "auth|sso|email_title": { + "en": "Login with ProConnect", + "fr": "Se connecter avec ProConnect" + }, + "auth|sso|proconnect_continue": { + "en": "Continue with ProConnect", + "fr": "Continuer avec ProConnect" + }, + "auth|sso|email_placeholder": { + "en": "Your professional email", + "fr": "Votre adresse mail professionelle" + }, + "welcome|sso|proconnect_explanation": { + "en": "What is ProConnect", + "fr": "Qu'est-ce que ProConnect" + }, + "auth|sso|error": { + "en": "An error occured during SSO login", + "fr": "Une erreur est survenue lors de la connexion" + }, + "auth|sso|error_homeserver": { + "en": "There is an error with the homeserver configuration", + "fr": "Il y a une erreur avec la configuration du serveur" + }, + "auth|sso|error_email": { + "en": "You need to enter your professional email", + "fr": "Vous devez entrer votre adresse professionelle" } } diff --git a/patches/subtree-modifications.json b/patches/subtree-modifications.json index ac5a44887c..f5dc88a0b7 100644 --- a/patches/subtree-modifications.json +++ b/patches/subtree-modifications.json @@ -78,5 +78,11 @@ "files": [ "src/components/views/settings/tabs/user/AccountUserSettingsTab.tsx" ] + }, + "sso-agentconnect-flow": { + "issue": "https://github.com/tchapgouv/tchap-web-v4/issues/386", + "files": [ + "src/components/structures/MatrixChat.tsx" + ] } } \ No newline at end of file diff --git a/res/css/views/sso/TchapSSO.pcss b/res/css/views/sso/TchapSSO.pcss new file mode 100644 index 0000000000..23f98c5604 --- /dev/null +++ b/res/css/views/sso/TchapSSO.pcss @@ -0,0 +1,44 @@ +form { + .tc_ButtonParent { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + padding: 14px 20px; + -webkit-box-orient: horizontal; + -webkit-box-direction: normal; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + -webkit-box-align: center; + -webkit-align-items: center; + -ms-flex-align: center; + align-items: center; + border-radius: 4px; + background-origin: content-box; + background-repeat: no-repeat; + background-position: 30px center; + text-decoration: none; + color: #2e2f32 !important; + width: 100%; + } + + .tc_ButtonProconnect { + background-color: #000091; + color: white !important; + margin-bottom: 40px; + } + + .tc_Button_iconPC { + background-image: url("../../../welcome/images/proconnect.svg"); + } + + .tc_bottomButton { + display: flex; + justify-content: center; + } +} \ No newline at end of file diff --git a/res/welcome.html b/res/welcome.html index 199c3afa04..cb29ddcc4a 100644 --- a/res/welcome.html +++ b/res/welcome.html @@ -105,7 +105,7 @@ } .mx_ButtonLabel { - margin-left: 20px; + margin-left: 30px; } .mx_Header_title { @@ -203,6 +203,69 @@ margin: 0 0 10px 0; } } + + /* :TCHAP: sso-agentconnect-flow*/ + + .tc_ButtonCol { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + } + + .tc_ButtonParent { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + padding: 14px 20px; + -webkit-box-orient: horizontal; + -webkit-box-direction: normal; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + -webkit-box-align: center; + -webkit-align-items: center; + -ms-flex-align: center; + align-items: center; + border-radius: 4px; + width: 175px; + background-repeat: no-repeat; + background-position: 10px center; + text-decoration: none; + color: #2e2f32 !important; + } + + .tc_ButtonProconnect { + background-color: #000091; + color: white !important; + margin-bottom: 10px; + } + + .tc_Button { + color: #000091 !important; + font-weight: bold; + } + + .tc_ButtonBorder { + outline: 1px solid #000091; + outline-offset: -2px; + margin-top: 20px; + margin-bottom: 10px; + } + .tc_ButtonProconnect_explanation { + + } + + .tc_Button_iconPC { + background-image: url("welcome/images/proconnect.svg"); + } + + /* end :TCHAP: */
@@ -228,14 +291,30 @@

_t("Welcome to Tchap")
_t("action|learn_more")

-
+ + + +
diff --git a/res/welcome/images/proconnect.svg b/res/welcome/images/proconnect.svg new file mode 100644 index 0000000000..41c99b526d --- /dev/null +++ b/res/welcome/images/proconnect.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/tchap/components/views/sso/EmailVerificationPage.tsx b/src/tchap/components/views/sso/EmailVerificationPage.tsx new file mode 100644 index 0000000000..d3c16b28b5 --- /dev/null +++ b/src/tchap/components/views/sso/EmailVerificationPage.tsx @@ -0,0 +1,159 @@ +/* +Copyright 2019 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React, { useState, useRef } from "react"; +import { _t, _td } from "matrix-react-sdk/src/languageHandler"; + +import AuthPage from "matrix-react-sdk/src/components/views/auth/AuthPage"; +import AuthBody from "matrix-react-sdk/src/components/views/auth/AuthBody"; +import AuthHeader from "matrix-react-sdk/src/components/views/auth/AuthHeader"; +import EmailField from "matrix-react-sdk/src/components/views/auth/EmailField"; +import Field from "matrix-react-sdk/src/components/views/elements/Field"; +import Spinner from "matrix-react-sdk/src/components/views/elements/Spinner"; +import AccessibleButton, { ButtonEvent } from "matrix-react-sdk/src/components/views/elements/AccessibleButton"; +import PlatformPeg from "matrix-react-sdk/src/PlatformPeg"; + +import { ErrorMessage } from "matrix-react-sdk/src/components/structures/ErrorMessage"; +import { SSOAction } from "matrix-js-sdk/src/matrix"; +import Login from "matrix-react-sdk/src/Login"; +import TchapUtils from "../../../util/TchapUtils"; +import { ValidatedServerConfig } from "matrix-react-sdk/src/utils/ValidatedServerConfig"; + +import "../../../../../res/css/views/sso/TchapSSO.pcss"; + +export default function EmailVerificationPage() { + + const [loading, setLoading] = useState(false); + const [email, setEmail] = useState(""); + const [errorText, setErrorText] = useState(""); + + const submitButtonChild = loading ? : _t("auth|sso|proconnect_continue"); + + const emailFieldRef = useRef(null); + + const displayError = (errorString: string): void => { + emailFieldRef.current?.focus(); + emailFieldRef.current?.validate({ allowEmpty: false, focused: true }); + setErrorText(errorString); + setLoading(false); + } + + const setUpCurrentHs = async (hs: Record): Promise => { + try { + const validatedServerConfig: ValidatedServerConfig = await TchapUtils.makeValidatedServerConfig(hs); + return validatedServerConfig; + } catch(err) { + window.location.assign("email-precheck-sso") + return null + } + + } + + const onSubmit = async (event: React.FormEvent): Promise => { + event.preventDefault(); + setLoading(true); + const isFieldCorrect = await emailFieldRef.current?.validate({ allowEmpty: false }); + + if (!isFieldCorrect) { + emailFieldRef.current?.focus(); + emailFieldRef.current?.validate({ allowEmpty: false, focused: true }); + setErrorText(_td("auth|sso|error_email")); + setLoading(false); + return; + } + + // check email domain and start sso with agentconnect + try { + // get user homeserver from his email + const hs: Record | void = await TchapUtils.fetchHomeserverForEmail(email); + if (!hs) { + displayError("This email address cannot be used in Tchap"); + return; + } + + const validatedServerConfig = await setUpCurrentHs(hs); + + if (!validatedServerConfig) { + displayError(_td("auth|sso|error_homeserver")); + return + } + + const login = new Login(hs.base_url, hs.base_url, null, {}); + + const matrixClient= login.createTemporaryClient(); + + // start SSO flow since we got the homeserver + PlatformPeg.get()?.startSingleSignOn(matrixClient, "sso", "/home", "", SSOAction.LOGIN); + + setLoading(false); + + } catch(err) { + displayError(_td("auth|sso|error")); + } + } + + const onInputChanged = (event: React.FormEvent) => { + setEmail(event.currentTarget.value); + } + + const onLoginByPasswordClick = () => { + window.location.assign("#/login"); + } + + return ( + + + +

+ {_t("auth|sso|email_title")} +

+
+
+
+ ) => onInputChanged(event)} + fieldRef={emailFieldRef} + /> +
+ {errorText && } + +
+ { + e.preventDefault(); + onLoginByPasswordClick(); + }} + > + {_t("auth|sso|sign_in_password_instead")} + +
+
+
+
+
+ ); +} From 4d321670f431726a2a1a17068f3db0ec2e41864a Mon Sep 17 00:00:00 2001 From: "marc.sirisak" Date: Mon, 30 Sep 2024 11:18:24 +0200 Subject: [PATCH 2/5] feat(sso): remove existing sso buttons --- .../src/components/structures/auth/Login.tsx | 3 ++- .../components/structures/auth/Registration.tsx | 4 +++- .../tchap-translations/tchap_translations.json | 4 ++-- res/welcome.html | 15 ++++++++------- 4 files changed, 15 insertions(+), 11 deletions(-) diff --git a/linked-dependencies/matrix-react-sdk/src/components/structures/auth/Login.tsx b/linked-dependencies/matrix-react-sdk/src/components/structures/auth/Login.tsx index 646b70ebda..2680ee2024 100644 --- a/linked-dependencies/matrix-react-sdk/src/components/structures/auth/Login.tsx +++ b/linked-dependencies/matrix-react-sdk/src/components/structures/auth/Login.tsx @@ -138,7 +138,8 @@ export default class LoginComponent extends React.PureComponent // eslint-disable-next-line @typescript-eslint/naming-convention "m.login.cas": () => this.renderSsoStep("cas"), // eslint-disable-next-line @typescript-eslint/naming-convention - "m.login.sso": () => this.renderSsoStep("sso"), + // :TCHAP: sso-agentconnect-flow + // "m.login.sso": () => this.renderSsoStep("sso"), "oidcNativeFlow": () => this.renderOidcNativeStep(), }; } diff --git a/linked-dependencies/matrix-react-sdk/src/components/structures/auth/Registration.tsx b/linked-dependencies/matrix-react-sdk/src/components/structures/auth/Registration.tsx index 9d72c1d92d..c5690092db 100644 --- a/linked-dependencies/matrix-react-sdk/src/components/structures/auth/Registration.tsx +++ b/linked-dependencies/matrix-react-sdk/src/components/structures/auth/Registration.tsx @@ -628,7 +628,9 @@ export default class Registration extends React.Component { return ( - {ssoSection} + {/* :TCHAP: sso-agentconnect-flow */} + {/* {ssoSection} */} + {/* end :TCHAP: */} What is ProConnect ?", + "fr": "-> Qu'est-ce que ProConnect ?" }, "auth|sso|error": { "en": "An error occured during SSO login", diff --git a/res/welcome.html b/res/welcome.html index cb29ddcc4a..bb57ec0ff2 100644 --- a/res/welcome.html +++ b/res/welcome.html @@ -258,13 +258,17 @@ margin-bottom: 10px; } .tc_ButtonProconnect_explanation { - + color: #000091 !important; } .tc_Button_iconPC { background-image: url("welcome/images/proconnect.svg"); } + .tc_paragraph a { + color: #2e2f32; + font-weight: bold; + } /* end :TCHAP: */ @@ -283,14 +287,11 @@

_t("Welcome to Tchap")
- - - -
diff --git a/res/welcome_sso.html b/res/welcome_sso.html new file mode 100644 index 0000000000..bb57ec0ff2 --- /dev/null +++ b/res/welcome_sso.html @@ -0,0 +1,327 @@ + + +
+ + + + +

_t("Welcome to Tchap")
+ _t("la messagerie instantanée de l'Administration") +

+ +
+ _t("Conçue et gérée par l'Administration française") + +
_t("action|learn_more")
+
+
+ +
\ No newline at end of file diff --git a/scripts/copy-res.js b/scripts/copy-res.js index c052e99546..a516134b5e 100755 --- a/scripts/copy-res.js +++ b/scripts/copy-res.js @@ -24,6 +24,9 @@ const COPY_LIST = [ ["res/manifest.json", "webapp"], ["res/sw.js", "webapp"], ["res/welcome.html", "webapp"], + // :TCHAP: sso-agentconnect-flow + ["res/welcome_sso.html", "webapp"], + // end :TCHAP: ["res/welcome/**", "webapp/welcome"], ["res/themes/**", "webapp/themes"], ["res/vector-icons/**", "webapp/vector-icons"], diff --git a/src/tchap/components/views/sso/EmailVerificationPage.tsx b/src/tchap/components/views/sso/EmailVerificationPage.tsx index d3c16b28b5..f8c1228534 100644 --- a/src/tchap/components/views/sso/EmailVerificationPage.tsx +++ b/src/tchap/components/views/sso/EmailVerificationPage.tsx @@ -120,8 +120,8 @@ export default function EmailVerificationPage() {

{_t("auth|sso|email_title")}

-
-
+ +
+ ); +} \ No newline at end of file diff --git a/src/tchap/util/TchapUIFeature.ts b/src/tchap/util/TchapUIFeature.ts index e3512dc3a5..1a966e6f60 100644 --- a/src/tchap/util/TchapUIFeature.ts +++ b/src/tchap/util/TchapUIFeature.ts @@ -51,5 +51,12 @@ export default class TchapUIFeature { return homeserversWithFeature.includes(userHomeServer!); } + // We separate from previous method, cause in this feature we cannot differenciate between homeserver since it is before the user connexion + public static isSSOFlowActive():boolean { + const ssoFlow : Record = SdkConfig.get("tchap_sso_flow") as Record ?? {isActive: false}; + + return ssoFlow.isActive; + } + } diff --git a/webpack.config.js b/webpack.config.js index 4b637168c7..e627c328f5 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -758,6 +758,9 @@ module.exports = (env, argv) => { "res/jitsi_external_api.min.js.LICENSE.txt", "res/manifest.json", "res/welcome.html", + // :TCHAP: sso-agentconnect-flow + "res/welcome_sso.html", + // end :TCHAP: { from: "welcome/**", context: path.resolve(__dirname, "res") }, { from: "themes/**", context: path.resolve(__dirname, "res") }, { from: "vector-icons/**", context: path.resolve(__dirname, "res") }, From 2ba5643fb97e5596d516a53fc8e521acfe995da6 Mon Sep 17 00:00:00 2001 From: "marc.sirisak" Date: Tue, 1 Oct 2024 19:34:57 +0200 Subject: [PATCH 4/5] test(sso): add test for email precheck, registration and welcome component --- config.preprod.json | 3 +- config.prod.json | 3 +- .../src/components/views/auth/Welcome.tsx | 1 + patches/subtree-modifications.json | 5 +- .../views/sso/EmailVerificationPage.tsx | 7 +- .../views/sso/EmailVerificationPage-test.tsx | 210 ++++++++++++++++++ .../components/views/sso/Register-test.tsx | 153 +++++++++++++ .../components/views/sso/Welcome-test.tsx | 49 ++++ 8 files changed, 421 insertions(+), 10 deletions(-) create mode 100644 test/unit-tests/tchap/components/views/sso/EmailVerificationPage-test.tsx create mode 100644 test/unit-tests/tchap/components/views/sso/Register-test.tsx create mode 100644 test/unit-tests/tchap/components/views/sso/Welcome-test.tsx diff --git a/config.preprod.json b/config.preprod.json index 4a3750b0f5..5864003de6 100644 --- a/config.preprod.json +++ b/config.preprod.json @@ -109,8 +109,7 @@ "feature_space": ["*"], "feature_audio_call": ["i.tchap.gouv.fr", "e.tchap.gouv.fr"], "feature_video_call": ["i.tchap.gouv.fr", "e.tchap.gouv.fr"], - "feature_screenshare_call": ["*"], - "feature_sso_flow": [] + "feature_screenshare_call": ["*"] }, "tchap_sso_flow": { "isActive": false diff --git a/config.prod.json b/config.prod.json index 05089e22bf..a7192ce50f 100644 --- a/config.prod.json +++ b/config.prod.json @@ -196,8 +196,7 @@ "feature_space": ["*"], "feature_audio_call": ["*"], "feature_video_call": ["agent.dinum.tchap.gouv.fr"], - "feature_screenshare_call": ["*"], - "feature_sso_flow": [] + "feature_screenshare_call": ["*"] }, "tchap_sso_flow": { "isActive": false diff --git a/linked-dependencies/matrix-react-sdk/src/components/views/auth/Welcome.tsx b/linked-dependencies/matrix-react-sdk/src/components/views/auth/Welcome.tsx index c2c0a8a01d..5d77ac175c 100644 --- a/linked-dependencies/matrix-react-sdk/src/components/views/auth/Welcome.tsx +++ b/linked-dependencies/matrix-react-sdk/src/components/views/auth/Welcome.tsx @@ -50,6 +50,7 @@ export default class Welcome extends React.PureComponent { replaceMap["$logoUrl"] = logoUrl; // :TCHAP: sso-agentconnect-flow - pageUrl = "welcome.html"; pageUrl = TchapUIFeature.isSSOFlowActive() ? "welcome_sso.html" : "welcome.html"; + // end :TCHAP: } return ( diff --git a/patches/subtree-modifications.json b/patches/subtree-modifications.json index f5dc88a0b7..878d86eb81 100644 --- a/patches/subtree-modifications.json +++ b/patches/subtree-modifications.json @@ -82,7 +82,10 @@ "sso-agentconnect-flow": { "issue": "https://github.com/tchapgouv/tchap-web-v4/issues/386", "files": [ - "src/components/structures/MatrixChat.tsx" + "src/components/structures/MatrixChat.tsx", + "src/components/structures/auth/Registration.tsx", + "src/components/structures/auth/Login.tsx", + "src/components/views/auth/Welcome.tsx" ] } } \ No newline at end of file diff --git a/src/tchap/components/views/sso/EmailVerificationPage.tsx b/src/tchap/components/views/sso/EmailVerificationPage.tsx index f8c1228534..d45b6fec8b 100644 --- a/src/tchap/components/views/sso/EmailVerificationPage.tsx +++ b/src/tchap/components/views/sso/EmailVerificationPage.tsx @@ -68,10 +68,7 @@ export default function EmailVerificationPage() { const isFieldCorrect = await emailFieldRef.current?.validate({ allowEmpty: false }); if (!isFieldCorrect) { - emailFieldRef.current?.focus(); - emailFieldRef.current?.validate({ allowEmpty: false, focused: true }); - setErrorText(_td("auth|sso|error_email")); - setLoading(false); + displayError(_td("auth|sso|error_email")); return; } @@ -135,7 +132,7 @@ export default function EmailVerificationPage() { />
{errorText && } -
diff --git a/test/unit-tests/tchap/components/views/sso/EmailVerificationPage-test.tsx b/test/unit-tests/tchap/components/views/sso/EmailVerificationPage-test.tsx new file mode 100644 index 0000000000..6754080eb7 --- /dev/null +++ b/test/unit-tests/tchap/components/views/sso/EmailVerificationPage-test.tsx @@ -0,0 +1,210 @@ +import React from "react"; +import { render, cleanup, fireEvent, screen, act } from "@testing-library/react"; +import { mocked, MockedObject } from "jest-mock"; +import { MatrixClient } from "matrix-js-sdk/src/matrix"; + +import EmailVerificationPage from "~tchap-web/src/tchap/components/views/sso/EmailVerificationPage"; +import TchapUtils from "~tchap-web/src/tchap/util/TchapUtils"; +import { ValidatedServerConfig } from "~matrix-react-sdk/src/utils/ValidatedServerConfig"; +import { mockPlatformPeg, stubClient } from "~matrix-react-sdk/test/test-utils"; +import BasePlatform from "~matrix-react-sdk/src/BasePlatform"; +import Login from "~matrix-react-sdk/src/Login"; + +jest.mock("~matrix-react-sdk/src/PlatformPeg"); +jest.mock("~tchap-web/src/tchap/util/TchapUtils"); +jest.mock("~matrix-react-sdk/src/Login"); + +describe("", () => { + const userEmail = "marc@tchap.beta.gouv.fr"; + const defaultHsUrl = "https://matrix.agent1.fr"; + const secondHsUrl = "https://matrix.agent2.fr"; + + const PlatformPegMocked: MockedObject = mockPlatformPeg(); + const mockedClient: MatrixClient = stubClient(); + const mockedTchapUtils = mocked(TchapUtils); + + const mockLoginObject = (hs: string = defaultHsUrl) => { + const mockLoginObject = mocked(new Login(hs, hs, null, {})); + mockLoginObject.createTemporaryClient.mockImplementation(() => mockedClient); + return mockLoginObject; + }; + + const mockedFetchHomeserverFromEmail = (hs: string = defaultHsUrl) => { + mockedTchapUtils.fetchHomeserverForEmail.mockImplementation(() => + Promise.resolve({ base_url: hs, server_name: hs }), + ); + }; + + const mockedValidatedServerConfig = (withError: boolean = false, hsUrl: string = defaultHsUrl) => { + if (withError) { + mockedTchapUtils.makeValidatedServerConfig.mockImplementation(() => { + throw new Error(); + }); + } else { + mockedTchapUtils.makeValidatedServerConfig.mockImplementation(() => + Promise.resolve({ + hsUrl: defaultHsUrl, + hsName: "hs", + hsNameIsDifferent: false, + isUrl: "", + isDefault: true, + isNameResolvable: true, + warning: "", + } as ValidatedServerConfig), + ); + } + }; + + const mockedPlatformPegStartSSO = (withError: boolean) => { + if (withError) { + jest.spyOn(PlatformPegMocked, "startSingleSignOn").mockImplementation(() => { + throw new Error(); + }); + } else { + jest.spyOn(PlatformPegMocked, "startSingleSignOn").mockImplementation(() => {}); + } + }; + + const renderEmailVerificationPage = () => render(); + + beforeEach(() => { + mockLoginObject(defaultHsUrl); + }); + + afterEach(() => { + cleanup(); + jest.restoreAllMocks(); + }); + + it("returns error when empty email", async () => { + const { container } = renderEmailVerificationPage(); + + // Put text in email field + const emailField = screen.getByRole("textbox"); + fireEvent.focus(emailField); + fireEvent.change(emailField, { target: { value: "" } }); + + // click on proconnect button + const proconnectButton = screen.getByTestId("proconnect-submit"); + await act(async () => { + await fireEvent.click(proconnectButton); + }); + + // Error classes should not appear + expect(container.getElementsByClassName("mx_ErrorMessage").length).toBe(1); + }); + + it("returns inccorrect email", async () => { + const { container } = renderEmailVerificationPage(); + + // Put text in email field + const emailField = screen.getByRole("textbox"); + fireEvent.focus(emailField); + fireEvent.change(emailField, { target: { value: "falseemail" } }); + + // click on proconnect button + const proconnectButton = screen.getByTestId("proconnect-submit"); + await act(async () => { + await fireEvent.click(proconnectButton); + }); + + // Error classes should not appear + expect(container.getElementsByClassName("mx_ErrorMessage").length).toBe(1); + }); + + it("should throw error when homeserver catch an error", async () => { + const { container } = renderEmailVerificationPage(); + + // mock server returns an errorn, we dont need to mock the other implementation + // since the code should throw an error before accessing them + mockedValidatedServerConfig(true); + + // Put text in email field + const emailField = screen.getByRole("textbox"); + fireEvent.focus(emailField); + fireEvent.change(emailField, { target: { value: userEmail } }); + + // click on proconnect button + const proconnectButton = screen.getByTestId("proconnect-submit"); + await act(async () => { + await fireEvent.click(proconnectButton); + }); + + // Error classes should not appear + expect(container.getElementsByClassName("mx_ErrorMessage").length).toBe(1); + }); + + it("should throw and error when connecting to proconnect error", async () => { + const { container } = renderEmailVerificationPage(); + + mockedValidatedServerConfig(false); + // mock platform page startsso error + mockedPlatformPegStartSSO(true); + + // Put text in email field + const emailField = screen.getByRole("textbox"); + fireEvent.focus(emailField); + fireEvent.change(emailField, { target: { value: userEmail } }); + + // click on proconnect button + const proconnectButton = screen.getByTestId("proconnect-submit"); + await act(async () => { + await fireEvent.click(proconnectButton); + }); + + // Error classes should not appear + expect(container.getElementsByClassName("mx_ErrorMessage").length).toBe(1); + }); + + it("should start sso with correct homeserver 1", async () => { + renderEmailVerificationPage(); + + // Mock the implementation without error, what we want is to be sure they are called with the correct parameters + mockedFetchHomeserverFromEmail(defaultHsUrl); + mockedValidatedServerConfig(false, defaultHsUrl); + mockedPlatformPegStartSSO(false); + + // Put text in email field + const emailField = screen.getByRole("textbox"); + fireEvent.focus(emailField); + fireEvent.change(emailField, { target: { value: userEmail } }); + + // click on proconnect button + const proconnectButton = screen.getByTestId("proconnect-submit"); + await act(async () => { + await fireEvent.click(proconnectButton); + }); + + expect(mockedTchapUtils.makeValidatedServerConfig).toHaveBeenCalledWith({ + base_url: defaultHsUrl, + server_name: defaultHsUrl, + }); + expect(PlatformPegMocked.startSingleSignOn).toHaveBeenCalledTimes(1); + }); + + it("should start sso with correct homeserver 2", async () => { + renderEmailVerificationPage(); + + // Mock the implementation without error, what we want is to be sure they are called with the correct parameters + mockedFetchHomeserverFromEmail(secondHsUrl); + mockedValidatedServerConfig(false, secondHsUrl); + mockedPlatformPegStartSSO(false); + + // Put text in email field + const emailField = screen.getByRole("textbox"); + fireEvent.focus(emailField); + fireEvent.change(emailField, { target: { value: userEmail } }); + + // click on proconnect button + const proconnectButton = screen.getByTestId("proconnect-submit"); + await act(async () => { + await fireEvent.click(proconnectButton); + }); + + expect(mockedTchapUtils.makeValidatedServerConfig).toHaveBeenCalledWith({ + base_url: secondHsUrl, + server_name: secondHsUrl, + }); + expect(PlatformPegMocked.startSingleSignOn).toHaveBeenCalledTimes(1); + }); +}); diff --git a/test/unit-tests/tchap/components/views/sso/Register-test.tsx b/test/unit-tests/tchap/components/views/sso/Register-test.tsx new file mode 100644 index 0000000000..53acdf57d7 --- /dev/null +++ b/test/unit-tests/tchap/components/views/sso/Register-test.tsx @@ -0,0 +1,153 @@ +import React from "react"; +import { render } from "@testing-library/react"; +import { mocked, MockedObject } from "jest-mock"; +import { MatrixClient, MatrixError, OidcClientConfig, createClient } from "matrix-js-sdk/src/matrix"; +import fetchMock from "fetch-mock"; + +import SdkConfig, { ConfigOptions, DEFAULTS } from "~matrix-react-sdk/src/SdkConfig"; +import Registration from "~matrix-react-sdk/src/components/structures/auth/Registration"; +import { + getMockClientWithEventEmitter, + mkServerConfig, + mockPlatformPeg, + unmockPlatformPeg, +} from "~matrix-react-sdk/test/test-utils"; +import { makeDelegatedAuthConfig } from "~matrix-react-sdk/test/test-utils/oidc"; +import SettingsStore from "~matrix-react-sdk/src/settings/SettingsStore"; +import AutoDiscoveryUtils from "~matrix-react-sdk/src/utils/AutoDiscoveryUtils"; +import { ValidatedServerConfig } from "~matrix-react-sdk/src/utils/ValidatedServerConfig"; + +jest.mock("~matrix-react-sdk/src/utils/oidc/authorize", () => ({ + startOidcLogin: jest.fn(), +})); + +jest.mock("matrix-js-sdk/src/matrix", () => ({ + ...jest.requireActual("matrix-js-sdk/src/matrix"), + createClient: jest.fn(), +})); + +/** The matrix versions our mock server claims to support */ +const SERVER_SUPPORTED_MATRIX_VERSIONS = ["v1.1", "v1.5", "v1.6", "v1.8", "v1.9"]; + +describe("", () => { + let mockClient!: MockedObject; + + const defaultHsUrl = "https://matrix.org"; + const defaultIsUrl = "https://vector.im"; + + const addSSOFlowToMockConfig = (isActive: boolean = false) => { + // mock SdkConfig.get("tchap_features") + const config: ConfigOptions = { tchap_sso_flow: { isActive } }; + SdkConfig.put(config); + }; + + const defaultProps = { + defaultDeviceDisplayName: "test-device-display-name", + onLoggedIn: jest.fn(), + onLoginClick: jest.fn(), + onServerConfigChange: jest.fn(), + }; + + function getRawComponent(hsUrl = defaultHsUrl, isUrl = defaultIsUrl, authConfig?: OidcClientConfig) { + return ; + } + + beforeEach(async function () { + const authConfig = makeDelegatedAuthConfig(); + // @ts-ignore + authConfig.metadata["prompt_values_supported"] = ["create"]; + + SdkConfig.put({ + ...DEFAULTS, + disable_custom_urls: true, + }); + mockClient = await getMockClientWithEventEmitter({ + registerRequest: jest.fn().mockImplementation( + () => + new MatrixError( + { + flows: [{ stages: [] }], + }, + 401, + ), + ), + loginFlows: jest.fn().mockResolvedValue({ flows: [{ type: "m.login.sso" }, { type: "m.login.password" }] }), + getVersions: jest.fn().mockResolvedValue({ versions: SERVER_SUPPORTED_MATRIX_VERSIONS }), + }); + + // used for registerRequest, but should return a MatrixError instance for the code to work... which is not the case here + fetchMock.catch({ + status: 401, + body: '{"errcode": "M_UNAUTHORIZE", "error": "Unauthorize request"}', + headers: { "content-type": "application/json" }, + }); + + // Doing this line can mock the request we want, but we want it to throw an error 401 which this doesnt do + // fetchMock.post(`${defaultHsUrl}/_matrix/client/v3/register`, { status: 401, type: "error" }); + + await mocked(createClient).mockImplementation((opts) => { + mockClient.idBaseUrl = opts.idBaseUrl; + mockClient.baseUrl = opts.baseUrl; + return mockClient; + }); + + fetchMock.get(`${defaultHsUrl}/_matrix/client/versions`, { + unstable_features: {}, + versions: SERVER_SUPPORTED_MATRIX_VERSIONS, + }); + + fetchMock.get(`${defaultHsUrl}/_matrix/client/unstable/org.matrix.msc2965/auth_issuer`, { + issuer: authConfig.metadata.issuer, + }); + + fetchMock.get("https://auth.org/.well-known/openid-configuration", authConfig.metadata); + + fetchMock.get(authConfig.metadata.jwks_uri!, { keys: [] }); + + fetchMock.get(`${defaultHsUrl}/_matrix/client/v3/login`, { + body: { flows: [{ type: "m.login.sso" }, { type: "m.login.password" }] }, + }); + + jest.spyOn(SettingsStore, "getValue").mockImplementation(() => false); + + jest.spyOn(AutoDiscoveryUtils, "validateServerConfigWithStaticUrls").mockResolvedValue({ + hsName: "example.com", + } as ValidatedServerConfig); + + mockPlatformPeg({ + startSingleSignOn: jest.fn(), + }); + }); + + afterEach(() => { + jest.restoreAllMocks(); + fetchMock.restore(); + SdkConfig.reset(); // we touch the config, so clean up + unmockPlatformPeg(); + }); + + /** TODO weird behavior of requestregister which the mock is not detected + * So it will al { + // addSSOFlowToMockConfig(true); + + // const { container } = render(getRawComponent()); + + // await waitForElementToBeRemoved(() => screen.queryAllByTestId("spinner")); + + // screen.debug(); + + // expect(container.getElementsByClassName("tc_pronnect").length).toBe(1); + // }); + + it("returns no proconnect button when the config does'nt include sso flow", () => { + addSSOFlowToMockConfig(false); + + const { container } = render(getRawComponent()); + + expect(container.getElementsByClassName("tc_pronnect").length).toBe(0); + }); +}); diff --git a/test/unit-tests/tchap/components/views/sso/Welcome-test.tsx b/test/unit-tests/tchap/components/views/sso/Welcome-test.tsx new file mode 100644 index 0000000000..2c08d9ccc3 --- /dev/null +++ b/test/unit-tests/tchap/components/views/sso/Welcome-test.tsx @@ -0,0 +1,49 @@ +import React from "react"; +import { render, cleanup, screen } from "@testing-library/react"; +import fetchMock from "fetch-mock"; + +import SdkConfig, { ConfigOptions } from "~matrix-react-sdk/src/SdkConfig"; +import Welcome from "~matrix-react-sdk/src/components/views/auth/Welcome"; +import { flushPromises } from "~matrix-react-sdk/test/test-utils"; + +describe("", () => { + const addSSOFlowToMockConfig = (isActive: boolean = false) => { + // mock SdkConfig.get("tchap_features") + const config: ConfigOptions = { tchap_sso_flow: { isActive } }; + SdkConfig.put(config); + }; + + const renderWelcomePage = () => render(); + + afterEach(() => { + cleanup(); + }); + + it("returns welcome_sso html when sso_flow is active in config ", async () => { + addSSOFlowToMockConfig(true); + + // we need to mock the call to the correct html page, since it is embeded in the component + // we don't need to mock the other html page since it shouldnt call it, otherwise it will simply throw an error + fetchMock.get("/welcome_sso.html", { body: "

SSO

" }); + + renderWelcomePage(); + await flushPromises(); + + // the component should choose the correct html page based on the sso_flo active value + expect(screen.getByRole("heading", { level: 1 }).textContent).toEqual("SSO"); + }); + + it("returns normal welcome html page without sso flow ", async () => { + addSSOFlowToMockConfig(false); + + // we need to mock the call to the correct html page, since it is embeded in the component + // we don't need to mock the other html page since it shouldnt call it, otherwise it will simply throw an error + fetchMock.get("/welcome.html", { body: "

Welcome

" }); + + renderWelcomePage(); + await flushPromises(); + + // the component should choose the correct html page based on the sso_flo active value + expect(screen.getByRole("heading", { level: 1 }).textContent).toEqual("Welcome"); + }); +}); From c42be0003b57fbc11d61a5419d7484f57d7ce2d7 Mon Sep 17 00:00:00 2001 From: "marc.sirisak" Date: Thu, 3 Oct 2024 10:18:47 +0200 Subject: [PATCH 5/5] chore(sso): cleanup welcome_sso html and css --- config.dev.json | 2 +- res/css/views/sso/TchapSSO.pcss | 2 +- res/welcome_sso.html | 43 +------------------ .../components/views/sso/ProconnectButton.tsx | 2 +- 4 files changed, 4 insertions(+), 45 deletions(-) diff --git a/config.dev.json b/config.dev.json index 3f23dbc15c..73627a934c 100644 --- a/config.dev.json +++ b/config.dev.json @@ -126,7 +126,7 @@ "feature_screenshare_call": ["*"] }, "tchap_sso_flow": { - "isActive": false + "isActive": true }, "map_style_url": "https://openmaptiles.geo.data.gouv.fr/styles/osm-bright/style.json", "element_call": { diff --git a/res/css/views/sso/TchapSSO.pcss b/res/css/views/sso/TchapSSO.pcss index e704d1c269..0ac6876071 100644 --- a/res/css/views/sso/TchapSSO.pcss +++ b/res/css/views/sso/TchapSSO.pcss @@ -27,7 +27,7 @@ } .tc_ButtonProconnect { - background-color: #000091; + background-color: var(--accent); color: white !important; } diff --git a/res/welcome_sso.html b/res/welcome_sso.html index bb57ec0ff2..154add7238 100644 --- a/res/welcome_sso.html +++ b/res/welcome_sso.html @@ -119,22 +119,11 @@ font-size: 18px; } - .mx_Header_subtitle { - font-size: 12px; - font-weight: normal; - margin: 8px 0 0; - } - .tc_paragraph { margin-bottom: 40px; margin-top: 40px; } - .mx_ButtonSignIn { - background-color: #000091; - color: white !important; - } - .mx_ButtonCreateAccount { background-color: #0dbd8b; color: white !important; @@ -147,21 +136,6 @@ margin-bottom: 40px; } - .mx_Button_iconSignIn { - background-image: url("welcome/images/icon-sign-in.svg"); - } - - .mx_Button_iconCreateAccount { - background-image: url("welcome/images/icon-create-account.svg"); - } - - .mx_Button_iconHelp { - background-image: url("welcome/images/icon-help.svg"); - } - - .mx_Button_iconRoomDirectory { - background-image: url("welcome/images/icon-room-directory.svg"); - } footer.mx_AuthFooter { background: rgba(255, 255, 255, 1) !important; @@ -233,7 +207,7 @@ -ms-flex-align: center; align-items: center; border-radius: 4px; - width: 175px; + width: 200px; background-repeat: no-repeat; background-position: 10px center; text-decoration: none; @@ -273,11 +247,6 @@
- @@ -292,15 +261,6 @@

_t("Welcome to Tchap")