diff --git a/src/ConferenceCallManagerHooks.jsx b/src/ConferenceCallManagerHooks.jsx index cfed40c..a433d14 100644 --- a/src/ConferenceCallManagerHooks.jsx +++ b/src/ConferenceCallManagerHooks.jsx @@ -394,7 +394,7 @@ export function ClientProvider({ children }) { client, loading: false, isAuthenticated: true, - isPasswordlessUser: false, + isPasswordlessUser: !!session.passwordlessUser, isGuest: false, userName: client.getUserIdLocalpart(), }); @@ -805,46 +805,68 @@ export function useInteractiveRegistration() { const privacyPolicyUrl = error.data?.params["m.login.terms"]?.policies?.privacy_policy?.en?.url; - if (privacyPolicyUrl) { - setState((prev) => ({ ...prev, privacyPolicyUrl })); + const recaptchaKey = error.data?.params["m.login.recaptcha"]?.public_key; + + if (privacyPolicyUrl || recaptchaKey) { + setState((prev) => ({ ...prev, privacyPolicyUrl, recaptchaKey })); } }); }, []); - const register = useCallback(async (username, password) => { - const interactiveAuth = new InteractiveAuth({ - matrixClient: authClientRef.current, - busyChanged(loading) { - setState((prev) => ({ ...prev, loading })); - }, - async doRequest(auth, _background) { - return authClientRef.current.registerRequest({ - username, - password, - auth: auth || undefined, - }); - }, - stateUpdated(nextStage, status) { - if (nextStage === "m.login.terms") { - interactiveAuth.submitAuthDict({ type: "m.login.terms" }); - } - }, - }); + const register = useCallback( + async (username, password, recaptchaResponse, passwordlessUser) => { + const interactiveAuth = new InteractiveAuth({ + matrixClient: authClientRef.current, + busyChanged(loading) { + setState((prev) => ({ ...prev, loading })); + }, + async doRequest(auth, _background) { + return authClientRef.current.registerRequest({ + username, + password, + auth: auth || undefined, + }); + }, + stateUpdated(nextStage, status) { + if (status.error) { + throw new Error(error); + } - const { user_id, access_token, device_id } = - await interactiveAuth.attemptAuth(); + if (nextStage === "m.login.terms") { + interactiveAuth.submitAuthDict({ + type: "m.login.terms", + }); + } else if (nextStage === "m.login.recaptcha") { + interactiveAuth.submitAuthDict({ + type: "m.login.recaptcha", + response: recaptchaResponse, + }); + } + }, + }); - const client = await initClient({ - baseUrl: defaultHomeserver, - accessToken: access_token, - userId: user_id, - deviceId: device_id, - }); + const { user_id, access_token, device_id } = + await interactiveAuth.attemptAuth(); - setClient(client, { user_id, access_token, device_id }); + const client = await initClient({ + baseUrl: defaultHomeserver, + accessToken: access_token, + userId: user_id, + deviceId: device_id, + }); - return client; - }, []); + const session = { user_id, device_id, access_token, passwordlessUser }; + + if (passwordlessUser) { + session.tempPassword = password; + } + + setClient(client, session); + + return client; + }, + [] + ); return [state, register]; } diff --git a/src/Home.jsx b/src/Home.jsx index e64c7db..3f9769e 100644 --- a/src/Home.jsx +++ b/src/Home.jsx @@ -35,6 +35,7 @@ import { ErrorView, LoadingView } from "./FullScreenView"; import { useModalTriggerState } from "./Modal"; import { randomString } from "matrix-js-sdk/src/randomstring"; import { JoinExistingCallModal } from "./JoinExistingCallModal"; +import { RecaptchaInput } from "./RecaptchaInput"; export function Home() { const { @@ -46,7 +47,9 @@ export function Home() { client, } = useClient(); - const [{ privacyPolicyUrl }, register] = useInteractiveRegistration(); + const [{ privacyPolicyUrl, recaptchaKey }, register] = + useInteractiveRegistration(); + const [recaptchaResponse, setRecaptchaResponse] = useState(); const history = useHistory(); const [creatingRoom, setCreatingRoom] = useState(false); @@ -64,8 +67,17 @@ export function Home() { async function onCreateRoom() { let _client = client; + if (!recaptchaResponse) { + return; + } + if (!_client || isGuest) { - _client = await register(userName, randomString(16), true); + _client = await register( + userName, + randomString(16), + recaptchaResponse, + true + ); } const roomIdOrAlias = await createRoom(_client, roomName); @@ -90,7 +102,7 @@ export function Home() { setCreatingRoom(false); }); }, - [client, history, register, isGuest] + [client, history, register, isGuest, recaptchaResponse] ); const onJoinRoom = useCallback( @@ -121,6 +133,8 @@ export function Home() { creatingRoom={creatingRoom} onJoinRoom={onJoinRoom} privacyPolicyUrl={privacyPolicyUrl} + recaptchaKey={recaptchaKey} + setRecaptchaResponse={setRecaptchaResponse} /> ) : ( + {recaptchaKey && ( + + + + )} {createRoomError && ( {createRoomError.message} diff --git a/src/RecaptchaInput.jsx b/src/RecaptchaInput.jsx new file mode 100644 index 0000000..1901196 --- /dev/null +++ b/src/RecaptchaInput.jsx @@ -0,0 +1,46 @@ +import React, { useEffect, useRef } from "react"; + +export function RecaptchaInput({ publicKey, onResponse }) { + const containerRef = useRef(); + const recaptchaRef = useRef(); + + useEffect(() => { + const onRecaptchaLoaded = () => { + if (!recaptchaRef.current) { + return; + } + + window.grecaptcha.render(recaptchaRef.current, { + sitekey: publicKey, + callback: (response) => { + if (!recaptchaRef.current) { + return; + } + + onResponse(response); + }, + }); + }; + + if ( + typeof window.grecaptcha !== "undefined" && + typeof window.grecaptcha.render === "function" + ) { + onRecaptchaLoaded(); + } else { + window.mxOnRecaptchaLoaded = onRecaptchaLoaded; + const scriptTag = document.createElement("script"); + scriptTag.setAttribute( + "src", + `https://www.recaptcha.net/recaptcha/api.js?onload=mxOnRecaptchaLoaded&render=explicit` + ); + containerRef.current.appendChild(scriptTag); + } + }, []); + + return ( +
+
+
+ ); +} diff --git a/src/RegisterPage.jsx b/src/RegisterPage.jsx index e79587c..81c6662 100644 --- a/src/RegisterPage.jsx +++ b/src/RegisterPage.jsx @@ -16,7 +16,7 @@ limitations under the License. import React, { useCallback, useEffect, useRef, useState } from "react"; import { useHistory, useLocation, Link } from "react-router-dom"; -import { FieldRow, InputField, ErrorMessage } from "./Input"; +import { FieldRow, InputField, ErrorMessage, Field } from "./Input"; import { Button } from "./button"; import { useClient, @@ -26,6 +26,7 @@ import { import styles from "./LoginPage.module.css"; import { ReactComponent as Logo } from "./icons/LogoLarge.svg"; import { LoadingView } from "./FullScreenView"; +import { RecaptchaInput } from "./RecaptchaInput"; export function RegisterPage() { const { @@ -44,7 +45,9 @@ export function RegisterPage() { const [password, setPassword] = useState(""); const [passwordConfirmation, setPasswordConfirmation] = useState(""); const [acceptTerms, setAcceptTerms] = useState(false); - const [{ privacyPolicyUrl }, register] = useInteractiveRegistration(); + const [{ privacyPolicyUrl, recaptchaKey }, register] = + useInteractiveRegistration(); + const [recaptchaResponse, setRecaptchaResponse] = useState(); const onSubmitRegisterForm = useCallback( (e) => { @@ -55,13 +58,13 @@ export function RegisterPage() { const passwordConfirmation = data.get("passwordConfirmation"); const acceptTerms = data.get("acceptTerms"); - if (password !== passwordConfirmation || !acceptTerms) { - return; - } - - setRegistering(true); - if (isPasswordlessUser) { + if (password !== passwordConfirmation) { + return; + } + + setRegistering(true); + changePassword(password) .then(() => { if (location.state && location.state.from) { @@ -75,7 +78,17 @@ export function RegisterPage() { setRegistering(false); }); } else { - register(userName, password) + if ( + password !== passwordConfirmation || + !acceptTerms || + !recaptchaResponse + ) { + return; + } + + setRegistering(true); + + register(userName, password, recaptchaResponse) .then(() => { if (location.state && location.state.from) { history.push(location.state.from); @@ -89,7 +102,14 @@ export function RegisterPage() { }); } }, - [register, changePassword, location, history, isPasswordlessUser] + [ + register, + changePassword, + location, + history, + isPasswordlessUser, + recaptchaResponse, + ] ); useEffect(() => { @@ -177,20 +197,30 @@ export function RegisterPage() { ref={confirmPasswordRef} /> - - setAcceptTerms(e.target.checked)} - checked={acceptTerms} - label="Accept Privacy Policy" - ref={acceptTermsRef} - /> - - Privacy Policy - - + {!isPasswordlessUser && ( + + setAcceptTerms(e.target.checked)} + checked={acceptTerms} + label="Accept Privacy Policy" + ref={acceptTermsRef} + /> + + Privacy Policy + + + )} + {!isPasswordlessUser && recaptchaKey && ( + + + + )} {error && ( {error.message}