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,
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];
}

View file

@ -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}
/>
) : (
<RegisteredView
@ -147,6 +161,8 @@ function UnregisteredView({
creatingRoom,
onJoinRoom,
privacyPolicyUrl,
recaptchaKey,
setRecaptchaResponse,
}) {
const acceptTermsRef = useRef();
const [acceptTerms, setAcceptTerms] = useState(false);
@ -237,6 +253,14 @@ function UnregisteredView({
Privacy Policy
</a>
</FieldRow>
{recaptchaKey && (
<FieldRow>
<RecaptchaInput
publicKey={recaptchaKey}
onResponse={setRecaptchaResponse}
/>
</FieldRow>
)}
{createRoomError && (
<FieldRow className={styles.fieldRow}>
<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 { 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}
/>
</FieldRow>
<FieldRow>
<InputField
id="acceptTerms"
type="checkbox"
name="acceptTerms"
onChange={(e) => setAcceptTerms(e.target.checked)}
checked={acceptTerms}
label="Accept Privacy Policy"
ref={acceptTermsRef}
/>
<a target="_blank" href={privacyPolicyUrl}>
Privacy Policy
</a>
</FieldRow>
{!isPasswordlessUser && (
<FieldRow>
<InputField
id="acceptTerms"
type="checkbox"
name="acceptTerms"
onChange={(e) => setAcceptTerms(e.target.checked)}
checked={acceptTerms}
label="Accept Privacy Policy"
ref={acceptTermsRef}
/>
<a target="_blank" href={privacyPolicyUrl}>
Privacy Policy
</a>
</FieldRow>
)}
{!isPasswordlessUser && recaptchaKey && (
<FieldRow>
<RecaptchaInput
publicKey={recaptchaKey}
onResponse={setRecaptchaResponse}
/>
</FieldRow>
)}
{error && (
<FieldRow>
<ErrorMessage>{error.message}</ErrorMessage>