Add change password flow for anonymous users

This commit is contained in:
Robert Long 2021-12-16 16:43:35 -08:00
parent d7fd06a250
commit a7539eb34e
5 changed files with 181 additions and 45 deletions

View file

@ -130,8 +130,14 @@ export function ClientProvider({ homeserverUrl, children }) {
const authStore = localStorage.getItem("matrix-auth-store"); const authStore = localStorage.getItem("matrix-auth-store");
if (authStore) { if (authStore) {
const { user_id, device_id, access_token, guest, passwordlessUser } = const {
JSON.parse(authStore); user_id,
device_id,
access_token,
guest,
passwordlessUser,
tempPassword,
} = JSON.parse(authStore);
const client = await initClient( const client = await initClient(
{ {
@ -151,6 +157,7 @@ export function ClientProvider({ homeserverUrl, children }) {
access_token, access_token,
guest, guest,
passwordlessUser, passwordlessUser,
tempPassword,
}) })
); );
@ -311,10 +318,13 @@ export function ClientProvider({ homeserverUrl, children }) {
deviceId: device_id, deviceId: device_id,
}); });
localStorage.setItem( const session = { user_id, device_id, access_token, passwordlessUser };
"matrix-auth-store",
JSON.stringify({ user_id, device_id, access_token, passwordlessUser }) if (passwordlessUser) {
); session.tempPassword = password;
}
localStorage.setItem("matrix-auth-store", JSON.stringify(session));
setState({ setState({
client, 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(() => { const logout = useCallback(() => {
localStorage.removeItem("matrix-auth-store"); localStorage.removeItem("matrix-auth-store");
window.location = "/"; window.location = "/";
@ -355,6 +404,7 @@ export function ClientProvider({ homeserverUrl, children }) {
login, login,
registerGuest, registerGuest,
register, register,
changePassword,
logout, logout,
userName, userName,
}), }),
@ -367,6 +417,7 @@ export function ClientProvider({ homeserverUrl, children }) {
login, login,
registerGuest, registerGuest,
register, register,
changePassword,
logout, logout,
userName, userName,
] ]

View file

@ -22,17 +22,30 @@ export function Field({ children, className, ...rest }) {
} }
export const InputField = forwardRef( export const InputField = forwardRef(
({ id, label, className, type, checked, prefix, suffix, ...rest }, ref) => { (
{ id, label, className, type, checked, prefix, suffix, disabled, ...rest },
ref
) => {
return ( return (
<Field <Field
className={classNames( className={classNames(
type === "checkbox" ? styles.checkboxField : styles.inputField, type === "checkbox" ? styles.checkboxField : styles.inputField,
{ [styles.prefix]: !!prefix }, {
[styles.prefix]: !!prefix,
[styles.disabled]: disabled,
},
className className
)} )}
> >
{prefix && <span>{prefix}</span>} {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}> <label htmlFor={id}>
{type === "checkbox" && ( {type === "checkbox" && (
<div className={styles.checkbox}> <div className={styles.checkbox}>

View file

@ -33,17 +33,26 @@
font-size: 15px; font-size: 15px;
border: none; border: none;
border-radius: 4px; border-radius: 4px;
padding: 11px 9px; padding: 12px 9px 10px 9px;
color: var(--textColor1); color: var(--textColor1);
background-color: var(--bgColor1); background-color: var(--bgColor1);
flex: 1; flex: 1;
min-width: 0; min-width: 0;
} }
.inputField.disabled input,
.inputField.disabled span {
color: var(--textColor2);
}
.inputField span { .inputField span {
padding: 11px 9px; padding: 11px 9px;
} }
.inputField span:first-child {
padding-right: 0;
}
.inputField input::placeholder { .inputField input::placeholder {
transition: color 0.25s ease-in 0s; transition: color 0.25s ease-in 0s;
color: transparent; color: transparent;

View file

@ -21,14 +21,21 @@ import { Button } from "./button";
import { useClient } from "./ConferenceCallManagerHooks"; import { useClient } from "./ConferenceCallManagerHooks";
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";
export function RegisterPage() { export function RegisterPage() {
const { register } = useClient(); const {
const registerUsernameRef = useRef(); loading,
client,
register,
changePassword,
isAuthenticated,
isPasswordlessUser,
} = useClient();
const confirmPasswordRef = useRef(); const confirmPasswordRef = useRef();
const history = useHistory(); const history = useHistory();
const location = useLocation(); const location = useLocation();
const [loading, setLoading] = useState(false); const [registering, setRegistering] = useState(false);
const [error, setError] = useState(); const [error, setError] = useState();
const [password, setPassword] = useState(""); const [password, setPassword] = useState("");
const [passwordConfirmation, setPasswordConfirmation] = useState(""); const [passwordConfirmation, setPasswordConfirmation] = useState("");
@ -36,11 +43,21 @@ export function RegisterPage() {
const onSubmitRegisterForm = useCallback( const onSubmitRegisterForm = useCallback(
(e) => { (e) => {
e.preventDefault(); e.preventDefault();
setLoading(true); const data = new FormData(e.target);
register( const userName = data.get("userName");
registerUsernameRef.current.value, const password = data.get("password");
registerPasswordRef.current.value const passwordConfirmation = data.get("passwordConfirmation");
)
if (password !== passwordConfirmation) {
return;
}
setRegistering(true);
console.log(isPasswordlessUser);
if (isPasswordlessUser) {
changePassword(password)
.then(() => { .then(() => {
if (location.state && location.state.from) { if (location.state && location.state.from) {
history.push(location.state.from); history.push(location.state.from);
@ -50,13 +67,31 @@ export function RegisterPage() {
}) })
.catch((error) => { .catch((error) => {
setError(error); setError(error);
setLoading(false); 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(() => { useEffect(() => {
if (!confirmPasswordRef.current) {
return;
}
if (password && passwordConfirmation && password !== passwordConfirmation) { if (password && passwordConfirmation && password !== passwordConfirmation) {
confirmPasswordRef.current.setCustomValidity("Passwords must match"); confirmPasswordRef.current.setCustomValidity("Passwords must match");
} else { } else {
@ -64,6 +99,16 @@ export function RegisterPage() {
} }
}, [password, passwordConfirmation]); }, [password, passwordConfirmation]);
useEffect(() => {
if (!loading && isAuthenticated && !isPasswordlessUser) {
history.push("/");
}
}, [history, isAuthenticated, isPasswordlessUser]);
if (loading) {
return <LoadingView />;
}
return ( return (
<> <>
<div className={styles.container}> <div className={styles.container}>
@ -75,18 +120,26 @@ export function RegisterPage() {
<FieldRow> <FieldRow>
<InputField <InputField
type="text" type="text"
ref={registerUsernameRef} name="userName"
placeholder="Username" placeholder="Username"
label="Username" label="Username"
autoCorrect="off" autoCorrect="off"
autoCapitalize="none" autoCapitalize="none"
prefix="@" prefix="@"
suffix={`:${window.location.host}`} suffix={`:${window.location.host}`}
value={
isAuthenticated && isPasswordlessUser
? client.getUserIdLocalpart()
: undefined
}
onChange={(e) => setUserName(e.target.value)}
disabled={isAuthenticated && isPasswordlessUser}
/> />
</FieldRow> </FieldRow>
<FieldRow> <FieldRow>
<InputField <InputField
required required
name="password"
type="password" type="password"
onChange={(e) => setPassword(e.target.value)} onChange={(e) => setPassword(e.target.value)}
value={password} value={password}
@ -98,6 +151,7 @@ export function RegisterPage() {
<InputField <InputField
required required
type="password" type="password"
name="passwordConfirmation"
onChange={(e) => setPasswordConfirmation(e.target.value)} onChange={(e) => setPasswordConfirmation(e.target.value)}
value={passwordConfirmation} value={passwordConfirmation}
placeholder="Confirm Password" placeholder="Confirm Password"
@ -111,8 +165,8 @@ export function RegisterPage() {
</FieldRow> </FieldRow>
)} )}
<FieldRow> <FieldRow>
<Button type="submit" disabled={loading}> <Button type="submit" disabled={registering}>
{loading ? "Registering..." : "Register"} {registering ? "Registering..." : "Register"}
</Button> </Button>
</FieldRow> </FieldRow>
</form> </form>

View file

@ -60,8 +60,15 @@ const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
export function Room() { export function Room() {
const [registeringGuest, setRegisteringGuest] = useState(false); const [registeringGuest, setRegisteringGuest] = useState(false);
const [registrationError, setRegistrationError] = useState(); const [registrationError, setRegistrationError] = useState();
const { loading, isAuthenticated, error, client, registerGuest, isGuest } = const {
useClient(); loading,
isAuthenticated,
error,
client,
registerGuest,
isGuest,
isPasswordlessUser,
} = useClient();
useEffect(() => { useEffect(() => {
if (!loading && !isAuthenticated) { if (!loading && !isAuthenticated) {
@ -86,10 +93,16 @@ export function Room() {
return <ErrorView error={registrationError || error} />; 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 { roomId: maybeRoomId } = useParams();
const { hash, search } = useLocation(); const { hash, search } = useLocation();
const [simpleGrid, viaServers] = useMemo(() => { const [simpleGrid, viaServers] = useMemo(() => {
@ -118,6 +131,7 @@ export function GroupCall({ client, isGuest }) {
return ( return (
<GroupCallView <GroupCallView
isGuest={isGuest} isGuest={isGuest}
isPasswordlessUser={isPasswordlessUser}
client={client} client={client}
roomId={roomId} roomId={roomId}
groupCall={groupCall} groupCall={groupCall}
@ -129,6 +143,7 @@ export function GroupCall({ client, isGuest }) {
export function GroupCallView({ export function GroupCallView({
client, client,
isGuest, isGuest,
isPasswordlessUser,
roomId, roomId,
groupCall, groupCall,
simpleGrid, simpleGrid,
@ -184,7 +199,7 @@ export function GroupCallView({
const onLeave = useCallback(() => { const onLeave = useCallback(() => {
leave(); leave();
if (!isGuest) { if (!isGuest && !isPasswordlessUser) {
history.push("/"); history.push("/");
} else { } else {
setLeft(true); setLeft(true);
@ -494,18 +509,12 @@ export function CallEndedScreen() {
<ul> <ul>
<li>Easily access all your previous call links</li> <li>Easily access all your previous call links</li>
<li>Set a username and avatar</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> </ul>
<LinkButton <LinkButton
className={styles.callEndedButton} className={styles.callEndedButton}
size="lg" size="lg"
variant="default" variant="default"
to="/login" to="/register"
> >
Create account Create account
</LinkButton> </LinkButton>