From 645cdab3dfe2a7355f6d461ec96c5581aa4f56ce Mon Sep 17 00:00:00 2001 From: "marc.sirisak" Date: Thu, 10 Oct 2024 18:41:30 +0200 Subject: [PATCH 1/2] feat(sso): add domain configured for sso check before login --- .../tchap_translations.json | 4 ++ .../views/sso/EmailVerificationPage.tsx | 24 ++++++++--- .../views/sso/EmailVerificationPage-test.tsx | 40 +++++++++++++++---- 3 files changed, 55 insertions(+), 13 deletions(-) diff --git a/modules/tchap-translations/tchap_translations.json b/modules/tchap-translations/tchap_translations.json index e73f2c2d3b..9049b593e3 100644 --- a/modules/tchap-translations/tchap_translations.json +++ b/modules/tchap-translations/tchap_translations.json @@ -873,5 +873,9 @@ "auth|proconnect|or": { "en": "or", "fr": "ou" + }, + "auth|proconnect|error_sso_inactive": { + "en": "ProConnect is not activated for your domain", + "fr": "Vous ne pouvez pas vous connecter avec ProConnect avec votre domaine" } } diff --git a/src/tchap/components/views/sso/EmailVerificationPage.tsx b/src/tchap/components/views/sso/EmailVerificationPage.tsx index ddf6aec81d..3527f77d40 100644 --- a/src/tchap/components/views/sso/EmailVerificationPage.tsx +++ b/src/tchap/components/views/sso/EmailVerificationPage.tsx @@ -62,13 +62,18 @@ export default function EmailVerificationPage() { } + const isSSOFlowActive = async (login: Login): Promise => { + const flows = await login.getFlows(); + return !!flows?.find((flow: Record) => flow.type === "m.login.sso"); + } + const onSubmit = async (event: React.FormEvent): Promise => { event.preventDefault(); setLoading(true); const isFieldCorrect = await emailFieldRef.current?.validate({ allowEmpty: false }); if (!isFieldCorrect) { - displayError(_td("auth|proconnect|error_email")); + displayError(_t("auth|proconnect|error_email")); return; } @@ -81,16 +86,23 @@ export default function EmailVerificationPage() { return; } + const login = new Login(hs.base_url, hs.base_url, null, {}); + + const matrixClient= login.createTemporaryClient(); + const validatedServerConfig = await setUpCurrentHs(hs); if (!validatedServerConfig) { - displayError(_td("auth|proconnect|error_homeserver")); + displayError(_t("auth|proconnect|error_homeserver")); return } - - const login = new Login(hs.base_url, hs.base_url, null, {}); - const matrixClient= login.createTemporaryClient(); + // check if oidc is activated on HS + const canSSO = await isSSOFlowActive(login); + if (!canSSO) { + displayError(_t("auth|proconnect|error_sso_inactive")); + return + } // start SSO flow since we got the homeserver PlatformPeg.get()?.startSingleSignOn(matrixClient, "sso", "/home", "", SSOAction.LOGIN); @@ -98,7 +110,7 @@ export default function EmailVerificationPage() { setLoading(false); } catch(err) { - displayError(_td("auth|proconnect|error")); + displayError(_t("auth|proconnect|error")); } } diff --git a/test/unit-tests/tchap/components/views/sso/EmailVerificationPage-test.tsx b/test/unit-tests/tchap/components/views/sso/EmailVerificationPage-test.tsx index 6754080eb7..c7e8bde175 100644 --- a/test/unit-tests/tchap/components/views/sso/EmailVerificationPage-test.tsx +++ b/test/unit-tests/tchap/components/views/sso/EmailVerificationPage-test.tsx @@ -22,12 +22,7 @@ describe("", () => { 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 mockedLogin = Login as jest.Mock; const mockedFetchHomeserverFromEmail = (hs: string = defaultHsUrl) => { mockedTchapUtils.fetchHomeserverForEmail.mockImplementation(() => @@ -68,7 +63,11 @@ describe("", () => { const renderEmailVerificationPage = () => render(); beforeEach(() => { - mockLoginObject(defaultHsUrl); + mockedLogin.mockImplementation(() => ({ + hsUrl: defaultHsUrl, + createTemporaryClient: jest.fn().mockReturnValue(mockedClient), + getFlows: jest.fn().mockResolvedValue([{ type: "m.login.sso" }]), + })); }); afterEach(() => { @@ -207,4 +206,31 @@ describe("", () => { }); expect(PlatformPegMocked.startSingleSignOn).toHaveBeenCalledTimes(1); }); + + it("should display error when sso is not configured in homeserer", async () => { + const { container } = 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); + // get flow without sso configured on homeserver + mockedLogin.mockImplementation(() => ({ + hsUrl: secondHsUrl, + createTemporaryClient: jest.fn().mockReturnValue(mockedClient), + getFlows: jest.fn().mockResolvedValue([{ type: "m.login.password" }]), + })); + // 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(container.getElementsByClassName("mx_ErrorMessage").length).toBe(1); + }); }); From 2dd6836d72c6ac5470db7242457ea92c6c1c93d4 Mon Sep 17 00:00:00 2001 From: "marc.sirisak" Date: Mon, 14 Oct 2024 19:30:51 +0200 Subject: [PATCH 2/2] feat(sso): add disabled button when email field invalid --- .../tchap_translations.json | 4 +-- .../views/sso/EmailVerificationPage.tsx | 36 ++++++++++++++----- .../views/sso/EmailVerificationPage-test.tsx | 24 +++++++++---- 3 files changed, 46 insertions(+), 18 deletions(-) diff --git a/modules/tchap-translations/tchap_translations.json b/modules/tchap-translations/tchap_translations.json index 9049b593e3..6b74005890 100644 --- a/modules/tchap-translations/tchap_translations.json +++ b/modules/tchap-translations/tchap_translations.json @@ -875,7 +875,7 @@ "fr": "ou" }, "auth|proconnect|error_sso_inactive": { - "en": "ProConnect is not activated for your domain", - "fr": "Vous ne pouvez pas vous connecter avec ProConnect avec votre domaine" + "en": "ProConnect is disabled for your domain", + "fr": "ProConnect est désactivé pour votre domaine" } } diff --git a/src/tchap/components/views/sso/EmailVerificationPage.tsx b/src/tchap/components/views/sso/EmailVerificationPage.tsx index 3527f77d40..71f8eb91f3 100644 --- a/src/tchap/components/views/sso/EmailVerificationPage.tsx +++ b/src/tchap/components/views/sso/EmailVerificationPage.tsx @@ -31,22 +31,26 @@ 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 * as Email from "matrix-react-sdk/src/email"; import "../../../../../res/css/views/sso/TchapSSO.pcss"; export default function EmailVerificationPage() { const [loading, setLoading] = useState(false); const [email, setEmail] = useState(""); + const [buttonDisabled, setButtonDisabled] = useState(true); const [errorText, setErrorText] = useState(""); const submitButtonChild = loading ? : _t("auth|proconnect|continue"); const emailFieldRef = useRef(null); + const checkEmailField = async (fieldString: string = email) : Promise => { + const fieldOk = await emailFieldRef.current?.validate({ allowEmpty: false, focused: true }); + return !!fieldOk && Email.looksValid(fieldString); + } + const displayError = (errorString: string): void => { - emailFieldRef.current?.focus(); - emailFieldRef.current?.validate({ allowEmpty: false, focused: true }); setErrorText(errorString); setLoading(false); } @@ -70,7 +74,7 @@ export default function EmailVerificationPage() { const onSubmit = async (event: React.FormEvent): Promise => { event.preventDefault(); setLoading(true); - const isFieldCorrect = await emailFieldRef.current?.validate({ allowEmpty: false }); + const isFieldCorrect = await checkEmailField(); if (!isFieldCorrect) { displayError(_t("auth|proconnect|error_email")); @@ -114,8 +118,11 @@ export default function EmailVerificationPage() { } } - const onInputChanged = (event: React.FormEvent) => { - setEmail(event.currentTarget.value); + const onInputChanged = async (event: React.FormEvent) => { + const emailString = event.currentTarget.value + setEmail(emailString); + const isEmailValid = await checkEmailField(emailString); + setButtonDisabled(!isEmailValid); } const onLoginByPasswordClick = () => { @@ -144,9 +151,20 @@ export default function EmailVerificationPage() { /> {errorText && } - + { + onSubmit(e); + }} + > + {submitButtonChild} +
", () => { }); it("returns error when empty email", async () => { - const { container } = renderEmailVerificationPage(); + renderEmailVerificationPage(); // Put text in email field const emailField = screen.getByRole("textbox"); @@ -85,16 +85,17 @@ describe("", () => { // 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); + // Submit button should be disabled + expect(proconnectButton).toHaveAttribute("disabled"); }); it("returns inccorrect email", async () => { - const { container } = renderEmailVerificationPage(); + renderEmailVerificationPage(); // Put text in email field const emailField = screen.getByRole("textbox"); @@ -107,8 +108,8 @@ describe("", () => { await fireEvent.click(proconnectButton); }); - // Error classes should not appear - expect(container.getElementsByClassName("mx_ErrorMessage").length).toBe(1); + // Submit button should be disabled + expect(proconnectButton).toHaveAttribute("disabled"); }); it("should throw error when homeserver catch an error", async () => { @@ -123,6 +124,7 @@ describe("", () => { fireEvent.focus(emailField); fireEvent.change(emailField, { target: { value: userEmail } }); + await flushPromises(); // click on proconnect button const proconnectButton = screen.getByTestId("proconnect-submit"); await act(async () => { @@ -145,6 +147,8 @@ describe("", () => { fireEvent.focus(emailField); fireEvent.change(emailField, { target: { value: userEmail } }); + await flushPromises(); + // click on proconnect button const proconnectButton = screen.getByTestId("proconnect-submit"); await act(async () => { @@ -168,6 +172,8 @@ describe("", () => { fireEvent.focus(emailField); fireEvent.change(emailField, { target: { value: userEmail } }); + await flushPromises(); + // click on proconnect button const proconnectButton = screen.getByTestId("proconnect-submit"); await act(async () => { @@ -194,6 +200,8 @@ describe("", () => { fireEvent.focus(emailField); fireEvent.change(emailField, { target: { value: userEmail } }); + await flushPromises(); + // click on proconnect button const proconnectButton = screen.getByTestId("proconnect-submit"); await act(async () => { @@ -225,6 +233,8 @@ describe("", () => { fireEvent.focus(emailField); fireEvent.change(emailField, { target: { value: userEmail } }); + await flushPromises(); + // click on proconnect button const proconnectButton = screen.getByTestId("proconnect-submit"); await act(async () => {