Refactor auth pages
This commit is contained in:
124
src/auth/LoginPage.jsx
Normal file
124
src/auth/LoginPage.jsx
Normal file
@@ -0,0 +1,124 @@
|
||||
/*
|
||||
Copyright 2021 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, { useCallback, useRef, useState, useMemo } from "react";
|
||||
import { useHistory, useLocation, Link } from "react-router-dom";
|
||||
import { ReactComponent as Logo } from "../icons/LogoLarge.svg";
|
||||
import { FieldRow, InputField, ErrorMessage } from "../Input";
|
||||
import { Button } from "../button";
|
||||
import {
|
||||
defaultHomeserver,
|
||||
defaultHomeserverHost,
|
||||
useInteractiveLogin,
|
||||
} from "../ConferenceCallManagerHooks";
|
||||
import styles from "./LoginPage.module.css";
|
||||
|
||||
export function LoginPage() {
|
||||
const [_, login] = useInteractiveLogin();
|
||||
const [homeserver, setHomeServer] = useState(defaultHomeserver);
|
||||
const usernameRef = useRef();
|
||||
const passwordRef = useRef();
|
||||
const history = useHistory();
|
||||
const location = useLocation();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState();
|
||||
|
||||
// TODO: Handle hitting login page with authenticated client
|
||||
|
||||
const onSubmitLoginForm = useCallback(
|
||||
(e) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
|
||||
login(homeserver, usernameRef.current.value, passwordRef.current.value)
|
||||
.then(() => {
|
||||
if (location.state && location.state.from) {
|
||||
history.push(location.state.from);
|
||||
} else {
|
||||
history.push("/");
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
setError(error);
|
||||
setLoading(false);
|
||||
});
|
||||
},
|
||||
[login, location, history, homeserver]
|
||||
);
|
||||
|
||||
const homeserverHost = useMemo(() => {
|
||||
try {
|
||||
return new URL(homeserver).host;
|
||||
} catch (_error) {
|
||||
return defaultHomeserverHost;
|
||||
}
|
||||
}, [homeserver]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.container}>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.formContainer}>
|
||||
<Logo width="auto" height="auto" className={styles.logo} />
|
||||
|
||||
<h2>Log In</h2>
|
||||
<h4>To continue to Element</h4>
|
||||
<form onSubmit={onSubmitLoginForm}>
|
||||
<FieldRow>
|
||||
<InputField
|
||||
type="text"
|
||||
ref={usernameRef}
|
||||
placeholder="Username"
|
||||
label="Username"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="none"
|
||||
prefix="@"
|
||||
suffix={`:${homeserverHost}`}
|
||||
/>
|
||||
</FieldRow>
|
||||
<FieldRow>
|
||||
<InputField
|
||||
type="password"
|
||||
ref={passwordRef}
|
||||
placeholder="Password"
|
||||
label="Password"
|
||||
/>
|
||||
</FieldRow>
|
||||
{error && (
|
||||
<FieldRow>
|
||||
<ErrorMessage>{error.message}</ErrorMessage>
|
||||
</FieldRow>
|
||||
)}
|
||||
<FieldRow>
|
||||
<Button type="submit" disabled={loading}>
|
||||
{loading ? "Logging in..." : "Login"}
|
||||
</Button>
|
||||
</FieldRow>
|
||||
</form>
|
||||
</div>
|
||||
<div className={styles.authLinks}>
|
||||
<p>Not registered yet?</p>
|
||||
<p>
|
||||
<Link to="/register">Create an account</Link>
|
||||
{" Or "}
|
||||
<Link to="/">Access as a guest</Link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
70
src/auth/LoginPage.module.css
Normal file
70
src/auth/LoginPage.module.css
Normal file
@@ -0,0 +1,70 @@
|
||||
.logo {
|
||||
max-width: 300px;
|
||||
margin: 80px 0;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 0 20px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 400px;
|
||||
width: 100%;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.formContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.formContainer h2,
|
||||
.formContainer h4 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.formContainer h4 {
|
||||
font-weight: normal;
|
||||
font-size: 18px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.formContainer form {
|
||||
width: 100%;
|
||||
margin-top: 32px;
|
||||
}
|
||||
|
||||
.formContainer button {
|
||||
height: 48px;
|
||||
width: 100%;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.authLinks {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.authLinks {
|
||||
margin-bottom: 100px;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.authLinks a {
|
||||
color: #0dbd8b;
|
||||
text-decoration: none;
|
||||
font-weight: normal;
|
||||
}
|
||||
211
src/auth/RegisterPage.jsx
Normal file
211
src/auth/RegisterPage.jsx
Normal file
@@ -0,0 +1,211 @@
|
||||
/*
|
||||
Copyright 2021 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, { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useHistory, useLocation } from "react-router-dom";
|
||||
import { FieldRow, InputField, ErrorMessage } from "../Input";
|
||||
import { Button } from "../button";
|
||||
import {
|
||||
useClient,
|
||||
defaultHomeserverHost,
|
||||
useInteractiveRegistration,
|
||||
} from "../ConferenceCallManagerHooks";
|
||||
import styles from "./LoginPage.module.css";
|
||||
import { ReactComponent as Logo } from "../icons/LogoLarge.svg";
|
||||
import { LoadingView } from "../FullScreenView";
|
||||
import { useRecaptcha } from "./useRecaptcha";
|
||||
import { Caption, Link } from "../typography/Typography";
|
||||
|
||||
export function RegisterPage() {
|
||||
const {
|
||||
loading,
|
||||
client,
|
||||
changePassword,
|
||||
isAuthenticated,
|
||||
isPasswordlessUser,
|
||||
} = useClient();
|
||||
const confirmPasswordRef = useRef();
|
||||
const history = useHistory();
|
||||
const location = useLocation();
|
||||
const [registering, setRegistering] = useState(false);
|
||||
const [error, setError] = useState();
|
||||
const [password, setPassword] = useState("");
|
||||
const [passwordConfirmation, setPasswordConfirmation] = useState("");
|
||||
const [{ privacyPolicyUrl, recaptchaKey }, register] =
|
||||
useInteractiveRegistration();
|
||||
const { execute, reset, recaptchaId } = useRecaptcha(recaptchaKey);
|
||||
|
||||
const onSubmitRegisterForm = useCallback(
|
||||
(e) => {
|
||||
e.preventDefault();
|
||||
const data = new FormData(e.target);
|
||||
const userName = data.get("userName");
|
||||
const password = data.get("password");
|
||||
const passwordConfirmation = data.get("passwordConfirmation");
|
||||
|
||||
if (password !== passwordConfirmation) {
|
||||
return;
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
setRegistering(true);
|
||||
|
||||
if (isPasswordlessUser) {
|
||||
changePassword(password);
|
||||
} else {
|
||||
const recaptchaResponse = await execute();
|
||||
await register(userName, password, recaptchaResponse);
|
||||
}
|
||||
}
|
||||
|
||||
submit()
|
||||
.then(() => {
|
||||
if (location.state && location.state.from) {
|
||||
history.push(location.state.from);
|
||||
} else {
|
||||
history.push("/");
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
setError(error);
|
||||
setRegistering(false);
|
||||
reset();
|
||||
});
|
||||
},
|
||||
[
|
||||
register,
|
||||
changePassword,
|
||||
location,
|
||||
history,
|
||||
isPasswordlessUser,
|
||||
reset,
|
||||
execute,
|
||||
]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!confirmPasswordRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (password && passwordConfirmation && password !== passwordConfirmation) {
|
||||
confirmPasswordRef.current.setCustomValidity("Passwords must match");
|
||||
} else {
|
||||
confirmPasswordRef.current.setCustomValidity("");
|
||||
}
|
||||
}, [password, passwordConfirmation]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!loading && isAuthenticated && !isPasswordlessUser) {
|
||||
history.push("/");
|
||||
}
|
||||
}, [history, isAuthenticated, isPasswordlessUser]);
|
||||
|
||||
if (loading) {
|
||||
return <LoadingView />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.container}>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.formContainer}>
|
||||
<Logo width="auto" height="auto" className={styles.logo} />
|
||||
<h2>Create your account</h2>
|
||||
<form onSubmit={onSubmitRegisterForm}>
|
||||
<FieldRow>
|
||||
<InputField
|
||||
type="text"
|
||||
name="userName"
|
||||
placeholder="Username"
|
||||
label="Username"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="none"
|
||||
prefix="@"
|
||||
suffix={`:${defaultHomeserverHost}`}
|
||||
value={
|
||||
isAuthenticated && isPasswordlessUser
|
||||
? client.getUserIdLocalpart()
|
||||
: undefined
|
||||
}
|
||||
disabled={isAuthenticated && isPasswordlessUser}
|
||||
/>
|
||||
</FieldRow>
|
||||
<FieldRow>
|
||||
<InputField
|
||||
required
|
||||
name="password"
|
||||
type="password"
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
value={password}
|
||||
placeholder="Password"
|
||||
label="Password"
|
||||
/>
|
||||
</FieldRow>
|
||||
<FieldRow>
|
||||
<InputField
|
||||
required
|
||||
type="password"
|
||||
name="passwordConfirmation"
|
||||
onChange={(e) => setPasswordConfirmation(e.target.value)}
|
||||
value={passwordConfirmation}
|
||||
placeholder="Confirm Password"
|
||||
label="Confirm Password"
|
||||
ref={confirmPasswordRef}
|
||||
/>
|
||||
</FieldRow>
|
||||
{!isPasswordlessUser && (
|
||||
<Caption>
|
||||
This site is protected by ReCAPTCHA and the Google{" "}
|
||||
<Link href="https://www.google.com/policies/privacy/">
|
||||
Privacy Policy
|
||||
</Link>{" "}
|
||||
and{" "}
|
||||
<Link href="https://policies.google.com/terms">
|
||||
Terms of Service
|
||||
</Link>{" "}
|
||||
apply.
|
||||
<br />
|
||||
By clicking "Go", you agree to our{" "}
|
||||
<Link href={privacyPolicyUrl}>Terms and conditions</Link>
|
||||
</Caption>
|
||||
)}
|
||||
{error && (
|
||||
<FieldRow>
|
||||
<ErrorMessage>{error.message}</ErrorMessage>
|
||||
</FieldRow>
|
||||
)}
|
||||
<FieldRow>
|
||||
<Button type="submit" disabled={registering}>
|
||||
{registering ? "Registering..." : "Register"}
|
||||
</Button>
|
||||
</FieldRow>
|
||||
<div id={recaptchaId} />
|
||||
</form>
|
||||
</div>
|
||||
<div className={styles.authLinks}>
|
||||
<p>Already have an account?</p>
|
||||
<p>
|
||||
<Link to="/login">Log in</Link>
|
||||
{" Or "}
|
||||
<Link to="/">Access as a guest</Link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
102
src/auth/useRecaptcha.js
Normal file
102
src/auth/useRecaptcha.js
Normal file
@@ -0,0 +1,102 @@
|
||||
import { randomString } from "matrix-js-sdk/src/randomstring";
|
||||
import { useEffect, useCallback, useRef, useState } from "react";
|
||||
|
||||
const RECAPTCHA_SCRIPT_URL =
|
||||
"https://www.recaptcha.net/recaptcha/api.js?onload=mxOnRecaptchaLoaded&render=explicit";
|
||||
|
||||
export function useRecaptcha(sitekey) {
|
||||
const [recaptchaId] = useState(() => randomString(16));
|
||||
const promiseRef = useRef();
|
||||
|
||||
useEffect(() => {
|
||||
if (!sitekey) {
|
||||
return;
|
||||
}
|
||||
|
||||
const onRecaptchaLoaded = () => {
|
||||
if (!document.getElementById(recaptchaId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.grecaptcha.render(recaptchaId, {
|
||||
sitekey,
|
||||
size: "invisible",
|
||||
callback: (response) => {
|
||||
if (promiseRef.current) {
|
||||
promiseRef.current.resolve(response);
|
||||
}
|
||||
},
|
||||
"error-callback": (error) => {
|
||||
if (promiseRef.current) {
|
||||
promiseRef.current.reject(error);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
if (
|
||||
typeof window.grecaptcha !== "undefined" &&
|
||||
typeof window.grecaptcha.render === "function"
|
||||
) {
|
||||
onRecaptchaLoaded();
|
||||
} else {
|
||||
window.mxOnRecaptchaLoaded = onRecaptchaLoaded;
|
||||
|
||||
if (!document.querySelector(`script[src="${RECAPTCHA_SCRIPT_URL}"]`)) {
|
||||
const scriptTag = document.createElement("script");
|
||||
scriptTag.src = RECAPTCHA_SCRIPT_URL;
|
||||
scriptTag.async = true;
|
||||
document.body.appendChild(scriptTag);
|
||||
}
|
||||
}
|
||||
}, [recaptchaId, sitekey]);
|
||||
|
||||
const execute = useCallback(() => {
|
||||
if (!window.grecaptcha) {
|
||||
return Promise.reject(new Error("Recaptcha not loaded"));
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const observer = new MutationObserver((mutationsList) => {
|
||||
for (const item of mutationsList) {
|
||||
if (item.target.style.visibility !== "visible") {
|
||||
reject(new Error("Recaptcha dismissed"));
|
||||
observer.disconnect();
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
promiseRef.current = {
|
||||
resolve: (value) => {
|
||||
resolve(value);
|
||||
observer.disconnect();
|
||||
},
|
||||
reject: (error) => {
|
||||
reject(error);
|
||||
observer.disconnect();
|
||||
},
|
||||
};
|
||||
|
||||
window.grecaptcha.execute();
|
||||
|
||||
const iframe = document.querySelector(
|
||||
'iframe[src*="recaptcha/api2/bframe"]'
|
||||
);
|
||||
|
||||
if (iframe?.parentNode?.parentNode) {
|
||||
observer.observe(iframe?.parentNode?.parentNode, {
|
||||
attributes: true,
|
||||
});
|
||||
}
|
||||
});
|
||||
}, [recaptchaId]);
|
||||
|
||||
const reset = useCallback(() => {
|
||||
if (window.grecaptcha) {
|
||||
window.grecaptcha.reset();
|
||||
}
|
||||
}, [recaptchaId]);
|
||||
|
||||
return { execute, reset, recaptchaId };
|
||||
}
|
||||
Reference in New Issue
Block a user