Add recaptcha

This commit is contained in:
Robert Long 2021-12-20 15:56:39 -08:00
parent d45d37b18a
commit ab73a351f8
4 changed files with 182 additions and 60 deletions

View file

@ -394,7 +394,7 @@ export function ClientProvider({ children }) {
client, client,
loading: false, loading: false,
isAuthenticated: true, isAuthenticated: true,
isPasswordlessUser: false, isPasswordlessUser: !!session.passwordlessUser,
isGuest: false, isGuest: false,
userName: client.getUserIdLocalpart(), userName: client.getUserIdLocalpart(),
}); });
@ -805,13 +805,16 @@ export function useInteractiveRegistration() {
const privacyPolicyUrl = const privacyPolicyUrl =
error.data?.params["m.login.terms"]?.policies?.privacy_policy?.en?.url; error.data?.params["m.login.terms"]?.policies?.privacy_policy?.en?.url;
if (privacyPolicyUrl) { const recaptchaKey = error.data?.params["m.login.recaptcha"]?.public_key;
setState((prev) => ({ ...prev, privacyPolicyUrl }));
if (privacyPolicyUrl || recaptchaKey) {
setState((prev) => ({ ...prev, privacyPolicyUrl, recaptchaKey }));
} }
}); });
}, []); }, []);
const register = useCallback(async (username, password) => { const register = useCallback(
async (username, password, recaptchaResponse, passwordlessUser) => {
const interactiveAuth = new InteractiveAuth({ const interactiveAuth = new InteractiveAuth({
matrixClient: authClientRef.current, matrixClient: authClientRef.current,
busyChanged(loading) { busyChanged(loading) {
@ -825,8 +828,19 @@ export function useInteractiveRegistration() {
}); });
}, },
stateUpdated(nextStage, status) { stateUpdated(nextStage, status) {
if (status.error) {
throw new Error(error);
}
if (nextStage === "m.login.terms") { if (nextStage === "m.login.terms") {
interactiveAuth.submitAuthDict({ type: "m.login.terms" }); interactiveAuth.submitAuthDict({
type: "m.login.terms",
});
} else if (nextStage === "m.login.recaptcha") {
interactiveAuth.submitAuthDict({
type: "m.login.recaptcha",
response: recaptchaResponse,
});
} }
}, },
}); });
@ -841,10 +855,18 @@ export function useInteractiveRegistration() {
deviceId: device_id, deviceId: device_id,
}); });
setClient(client, { user_id, access_token, device_id }); const session = { user_id, device_id, access_token, passwordlessUser };
if (passwordlessUser) {
session.tempPassword = password;
}
setClient(client, session);
return client; return client;
}, []); },
[]
);
return [state, register]; return [state, register];
} }

View file

@ -35,6 +35,7 @@ import { ErrorView, LoadingView } from "./FullScreenView";
import { useModalTriggerState } from "./Modal"; import { useModalTriggerState } from "./Modal";
import { randomString } from "matrix-js-sdk/src/randomstring"; import { randomString } from "matrix-js-sdk/src/randomstring";
import { JoinExistingCallModal } from "./JoinExistingCallModal"; import { JoinExistingCallModal } from "./JoinExistingCallModal";
import { RecaptchaInput } from "./RecaptchaInput";
export function Home() { export function Home() {
const { const {
@ -46,7 +47,9 @@ export function Home() {
client, client,
} = useClient(); } = useClient();
const [{ privacyPolicyUrl }, register] = useInteractiveRegistration(); const [{ privacyPolicyUrl, recaptchaKey }, register] =
useInteractiveRegistration();
const [recaptchaResponse, setRecaptchaResponse] = useState();
const history = useHistory(); const history = useHistory();
const [creatingRoom, setCreatingRoom] = useState(false); const [creatingRoom, setCreatingRoom] = useState(false);
@ -64,8 +67,17 @@ export function Home() {
async function onCreateRoom() { async function onCreateRoom() {
let _client = client; let _client = client;
if (!recaptchaResponse) {
return;
}
if (!_client || isGuest) { if (!_client || isGuest) {
_client = await register(userName, randomString(16), true); _client = await register(
userName,
randomString(16),
recaptchaResponse,
true
);
} }
const roomIdOrAlias = await createRoom(_client, roomName); const roomIdOrAlias = await createRoom(_client, roomName);
@ -90,7 +102,7 @@ export function Home() {
setCreatingRoom(false); setCreatingRoom(false);
}); });
}, },
[client, history, register, isGuest] [client, history, register, isGuest, recaptchaResponse]
); );
const onJoinRoom = useCallback( const onJoinRoom = useCallback(
@ -121,6 +133,8 @@ export function Home() {
creatingRoom={creatingRoom} creatingRoom={creatingRoom}
onJoinRoom={onJoinRoom} onJoinRoom={onJoinRoom}
privacyPolicyUrl={privacyPolicyUrl} privacyPolicyUrl={privacyPolicyUrl}
recaptchaKey={recaptchaKey}
setRecaptchaResponse={setRecaptchaResponse}
/> />
) : ( ) : (
<RegisteredView <RegisteredView
@ -147,6 +161,8 @@ function UnregisteredView({
creatingRoom, creatingRoom,
onJoinRoom, onJoinRoom,
privacyPolicyUrl, privacyPolicyUrl,
recaptchaKey,
setRecaptchaResponse,
}) { }) {
const acceptTermsRef = useRef(); const acceptTermsRef = useRef();
const [acceptTerms, setAcceptTerms] = useState(false); const [acceptTerms, setAcceptTerms] = useState(false);
@ -237,6 +253,14 @@ function UnregisteredView({
Privacy Policy Privacy Policy
</a> </a>
</FieldRow> </FieldRow>
{recaptchaKey && (
<FieldRow>
<RecaptchaInput
publicKey={recaptchaKey}
onResponse={setRecaptchaResponse}
/>
</FieldRow>
)}
{createRoomError && ( {createRoomError && (
<FieldRow className={styles.fieldRow}> <FieldRow className={styles.fieldRow}>
<ErrorMessage>{createRoomError.message}</ErrorMessage> <ErrorMessage>{createRoomError.message}</ErrorMessage>

46
src/RecaptchaInput.jsx Normal file
View file

@ -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 (
<div ref={containerRef}>
<div ref={recaptchaRef} />
</div>
);
}

View file

@ -16,7 +16,7 @@ limitations under the License.
import React, { useCallback, useEffect, useRef, useState } from "react"; import React, { useCallback, useEffect, useRef, useState } from "react";
import { useHistory, useLocation, Link } from "react-router-dom"; 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 { Button } from "./button";
import { import {
useClient, useClient,
@ -26,6 +26,7 @@ import {
import styles from "./LoginPage.module.css"; import styles from "./LoginPage.module.css";
import { ReactComponent as Logo } from "./icons/LogoLarge.svg"; import { ReactComponent as Logo } from "./icons/LogoLarge.svg";
import { LoadingView } from "./FullScreenView"; import { LoadingView } from "./FullScreenView";
import { RecaptchaInput } from "./RecaptchaInput";
export function RegisterPage() { export function RegisterPage() {
const { const {
@ -44,7 +45,9 @@ export function RegisterPage() {
const [password, setPassword] = useState(""); const [password, setPassword] = useState("");
const [passwordConfirmation, setPasswordConfirmation] = useState(""); const [passwordConfirmation, setPasswordConfirmation] = useState("");
const [acceptTerms, setAcceptTerms] = useState(false); const [acceptTerms, setAcceptTerms] = useState(false);
const [{ privacyPolicyUrl }, register] = useInteractiveRegistration(); const [{ privacyPolicyUrl, recaptchaKey }, register] =
useInteractiveRegistration();
const [recaptchaResponse, setRecaptchaResponse] = useState();
const onSubmitRegisterForm = useCallback( const onSubmitRegisterForm = useCallback(
(e) => { (e) => {
@ -55,13 +58,13 @@ export function RegisterPage() {
const passwordConfirmation = data.get("passwordConfirmation"); const passwordConfirmation = data.get("passwordConfirmation");
const acceptTerms = data.get("acceptTerms"); const acceptTerms = data.get("acceptTerms");
if (password !== passwordConfirmation || !acceptTerms) { if (isPasswordlessUser) {
if (password !== passwordConfirmation) {
return; return;
} }
setRegistering(true); setRegistering(true);
if (isPasswordlessUser) {
changePassword(password) changePassword(password)
.then(() => { .then(() => {
if (location.state && location.state.from) { if (location.state && location.state.from) {
@ -75,7 +78,17 @@ export function RegisterPage() {
setRegistering(false); setRegistering(false);
}); });
} else { } else {
register(userName, password) if (
password !== passwordConfirmation ||
!acceptTerms ||
!recaptchaResponse
) {
return;
}
setRegistering(true);
register(userName, password, recaptchaResponse)
.then(() => { .then(() => {
if (location.state && location.state.from) { if (location.state && location.state.from) {
history.push(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(() => { useEffect(() => {
@ -177,6 +197,7 @@ export function RegisterPage() {
ref={confirmPasswordRef} ref={confirmPasswordRef}
/> />
</FieldRow> </FieldRow>
{!isPasswordlessUser && (
<FieldRow> <FieldRow>
<InputField <InputField
id="acceptTerms" id="acceptTerms"
@ -191,6 +212,15 @@ export function RegisterPage() {
Privacy Policy Privacy Policy
</a> </a>
</FieldRow> </FieldRow>
)}
{!isPasswordlessUser && recaptchaKey && (
<FieldRow>
<RecaptchaInput
publicKey={recaptchaKey}
onResponse={setRecaptchaResponse}
/>
</FieldRow>
)}
{error && ( {error && (
<FieldRow> <FieldRow>
<ErrorMessage>{error.message}</ErrorMessage> <ErrorMessage>{error.message}</ErrorMessage>