From a7539eb34e7dfb538d7ad31f25760d8d940d19f2 Mon Sep 17 00:00:00 2001 From: Robert Long <robert@robertlong.me> Date: Thu, 16 Dec 2021 16:43:35 -0800 Subject: [PATCH] Add change password flow for anonymous users --- src/ConferenceCallManagerHooks.jsx | 63 ++++++++++++++++-- src/Input.jsx | 19 +++++- src/Input.module.css | 11 +++- src/RegisterPage.jsx | 100 ++++++++++++++++++++++------- src/Room.jsx | 33 ++++++---- 5 files changed, 181 insertions(+), 45 deletions(-) diff --git a/src/ConferenceCallManagerHooks.jsx b/src/ConferenceCallManagerHooks.jsx index b559a25..2e5d32d 100644 --- a/src/ConferenceCallManagerHooks.jsx +++ b/src/ConferenceCallManagerHooks.jsx @@ -130,8 +130,14 @@ export function ClientProvider({ homeserverUrl, children }) { const authStore = localStorage.getItem("matrix-auth-store"); if (authStore) { - const { user_id, device_id, access_token, guest, passwordlessUser } = - JSON.parse(authStore); + const { + user_id, + device_id, + access_token, + guest, + passwordlessUser, + tempPassword, + } = JSON.parse(authStore); const client = await initClient( { @@ -151,6 +157,7 @@ export function ClientProvider({ homeserverUrl, children }) { access_token, guest, passwordlessUser, + tempPassword, }) ); @@ -311,10 +318,13 @@ export function ClientProvider({ homeserverUrl, children }) { deviceId: device_id, }); - localStorage.setItem( - "matrix-auth-store", - JSON.stringify({ user_id, device_id, access_token, passwordlessUser }) - ); + const session = { user_id, device_id, access_token, passwordlessUser }; + + if (passwordlessUser) { + session.tempPassword = password; + } + + localStorage.setItem("matrix-auth-store", JSON.stringify(session)); setState({ client, @@ -340,6 +350,45 @@ export function ClientProvider({ homeserverUrl, children }) { } }, []); + const changePassword = useCallback( + async (password) => { + const { tempPassword, passwordlessUser, ...existingSession } = JSON.parse( + localStorage.getItem("matrix-auth-store") + ); + + await client.setPassword( + { + type: "m.login.password", + identifier: { + type: "m.id.user", + user: existingSession.user_id, + }, + user: existingSession.user_id, + password: tempPassword, + }, + password + ); + + localStorage.setItem( + "matrix-auth-store", + JSON.stringify({ + ...existingSession, + passwordlessUser: false, + }) + ); + + setState({ + client, + loading: false, + isGuest: false, + isAuthenticated: true, + isPasswordlessUser: false, + userName: client.getUserIdLocalpart(), + }); + }, + [client] + ); + const logout = useCallback(() => { localStorage.removeItem("matrix-auth-store"); window.location = "/"; @@ -355,6 +404,7 @@ export function ClientProvider({ homeserverUrl, children }) { login, registerGuest, register, + changePassword, logout, userName, }), @@ -367,6 +417,7 @@ export function ClientProvider({ homeserverUrl, children }) { login, registerGuest, register, + changePassword, logout, userName, ] diff --git a/src/Input.jsx b/src/Input.jsx index d890ab3..b9ccdc1 100644 --- a/src/Input.jsx +++ b/src/Input.jsx @@ -22,17 +22,30 @@ export function Field({ children, className, ...rest }) { } export const InputField = forwardRef( - ({ id, label, className, type, checked, prefix, suffix, ...rest }, ref) => { + ( + { id, label, className, type, checked, prefix, suffix, disabled, ...rest }, + ref + ) => { return ( <Field className={classNames( type === "checkbox" ? styles.checkboxField : styles.inputField, - { [styles.prefix]: !!prefix }, + { + [styles.prefix]: !!prefix, + [styles.disabled]: disabled, + }, className )} > {prefix && <span>{prefix}</span>} - <input id={id} {...rest} ref={ref} type={type} checked={checked} /> + <input + id={id} + {...rest} + ref={ref} + type={type} + checked={checked} + disabled={disabled} + /> <label htmlFor={id}> {type === "checkbox" && ( <div className={styles.checkbox}> diff --git a/src/Input.module.css b/src/Input.module.css index b47a20d..de54638 100644 --- a/src/Input.module.css +++ b/src/Input.module.css @@ -33,17 +33,26 @@ font-size: 15px; border: none; border-radius: 4px; - padding: 11px 9px; + padding: 12px 9px 10px 9px; color: var(--textColor1); background-color: var(--bgColor1); flex: 1; min-width: 0; } +.inputField.disabled input, +.inputField.disabled span { + color: var(--textColor2); +} + .inputField span { padding: 11px 9px; } +.inputField span:first-child { + padding-right: 0; +} + .inputField input::placeholder { transition: color 0.25s ease-in 0s; color: transparent; diff --git a/src/RegisterPage.jsx b/src/RegisterPage.jsx index a5cf3bb..fe54371 100644 --- a/src/RegisterPage.jsx +++ b/src/RegisterPage.jsx @@ -21,14 +21,21 @@ import { Button } from "./button"; import { useClient } from "./ConferenceCallManagerHooks"; import styles from "./LoginPage.module.css"; import { ReactComponent as Logo } from "./icons/LogoLarge.svg"; +import { LoadingView } from "./FullScreenView"; export function RegisterPage() { - const { register } = useClient(); - const registerUsernameRef = useRef(); + const { + loading, + client, + register, + changePassword, + isAuthenticated, + isPasswordlessUser, + } = useClient(); const confirmPasswordRef = useRef(); const history = useHistory(); const location = useLocation(); - const [loading, setLoading] = useState(false); + const [registering, setRegistering] = useState(false); const [error, setError] = useState(); const [password, setPassword] = useState(""); const [passwordConfirmation, setPasswordConfirmation] = useState(""); @@ -36,27 +43,55 @@ export function RegisterPage() { const onSubmitRegisterForm = useCallback( (e) => { e.preventDefault(); - setLoading(true); - register( - registerUsernameRef.current.value, - registerPasswordRef.current.value - ) - .then(() => { - if (location.state && location.state.from) { - history.push(location.state.from); - } else { - history.push("/"); - } - }) - .catch((error) => { - setError(error); - setLoading(false); - }); + 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; + } + + setRegistering(true); + + console.log(isPasswordlessUser); + + if (isPasswordlessUser) { + changePassword(password) + .then(() => { + if (location.state && location.state.from) { + history.push(location.state.from); + } else { + history.push("/"); + } + }) + .catch((error) => { + setError(error); + setRegistering(false); + }); + } else { + register(userName, password) + .then(() => { + if (location.state && location.state.from) { + history.push(location.state.from); + } else { + history.push("/"); + } + }) + .catch((error) => { + setError(error); + setRegistering(false); + }); + } }, - [register, location, history] + [register, changePassword, location, history, isPasswordlessUser] ); useEffect(() => { + if (!confirmPasswordRef.current) { + return; + } + if (password && passwordConfirmation && password !== passwordConfirmation) { confirmPasswordRef.current.setCustomValidity("Passwords must match"); } else { @@ -64,6 +99,16 @@ export function RegisterPage() { } }, [password, passwordConfirmation]); + useEffect(() => { + if (!loading && isAuthenticated && !isPasswordlessUser) { + history.push("/"); + } + }, [history, isAuthenticated, isPasswordlessUser]); + + if (loading) { + return <LoadingView />; + } + return ( <> <div className={styles.container}> @@ -75,18 +120,26 @@ export function RegisterPage() { <FieldRow> <InputField type="text" - ref={registerUsernameRef} + name="userName" placeholder="Username" label="Username" autoCorrect="off" autoCapitalize="none" prefix="@" suffix={`:${window.location.host}`} + value={ + isAuthenticated && isPasswordlessUser + ? client.getUserIdLocalpart() + : undefined + } + onChange={(e) => setUserName(e.target.value)} + disabled={isAuthenticated && isPasswordlessUser} /> </FieldRow> <FieldRow> <InputField required + name="password" type="password" onChange={(e) => setPassword(e.target.value)} value={password} @@ -98,6 +151,7 @@ export function RegisterPage() { <InputField required type="password" + name="passwordConfirmation" onChange={(e) => setPasswordConfirmation(e.target.value)} value={passwordConfirmation} placeholder="Confirm Password" @@ -111,8 +165,8 @@ export function RegisterPage() { </FieldRow> )} <FieldRow> - <Button type="submit" disabled={loading}> - {loading ? "Registering..." : "Register"} + <Button type="submit" disabled={registering}> + {registering ? "Registering..." : "Register"} </Button> </FieldRow> </form> diff --git a/src/Room.jsx b/src/Room.jsx index 0d2ed56..dc9f5f2 100644 --- a/src/Room.jsx +++ b/src/Room.jsx @@ -60,8 +60,15 @@ const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent); export function Room() { const [registeringGuest, setRegisteringGuest] = useState(false); const [registrationError, setRegistrationError] = useState(); - const { loading, isAuthenticated, error, client, registerGuest, isGuest } = - useClient(); + const { + loading, + isAuthenticated, + error, + client, + registerGuest, + isGuest, + isPasswordlessUser, + } = useClient(); useEffect(() => { if (!loading && !isAuthenticated) { @@ -86,10 +93,16 @@ export function Room() { return <ErrorView error={registrationError || error} />; } - return <GroupCall client={client} isGuest={isGuest} />; + return ( + <GroupCall + client={client} + isGuest={isGuest} + isPasswordlessUser={isPasswordlessUser} + /> + ); } -export function GroupCall({ client, isGuest }) { +export function GroupCall({ client, isGuest, isPasswordlessUser }) { const { roomId: maybeRoomId } = useParams(); const { hash, search } = useLocation(); const [simpleGrid, viaServers] = useMemo(() => { @@ -118,6 +131,7 @@ export function GroupCall({ client, isGuest }) { return ( <GroupCallView isGuest={isGuest} + isPasswordlessUser={isPasswordlessUser} client={client} roomId={roomId} groupCall={groupCall} @@ -129,6 +143,7 @@ export function GroupCall({ client, isGuest }) { export function GroupCallView({ client, isGuest, + isPasswordlessUser, roomId, groupCall, simpleGrid, @@ -184,7 +199,7 @@ export function GroupCallView({ const onLeave = useCallback(() => { leave(); - if (!isGuest) { + if (!isGuest && !isPasswordlessUser) { history.push("/"); } else { setLeft(true); @@ -494,18 +509,12 @@ export function CallEndedScreen() { <ul> <li>Easily access all your previous call links</li> <li>Set a username and avatar</li> - <li> - Get your own Matrix ID so you can log in to{" "} - <a href="https://element.io" target="_blank"> - Element - </a> - </li> </ul> <LinkButton className={styles.callEndedButton} size="lg" variant="default" - to="/login" + to="/register" > Create account </LinkButton>