mirror of
https://github.com/cinnyapp/cinny.git
synced 2025-11-15 11:40:29 +03:00
feat: URL navigation in auth (#1603)
* bump to react 18 and install react-router-dom * Upgrade to react 18 root * update vite * add cs api's * convert state/auth to ts * add client config context * add auto discovery context * add spec version context * add auth flow context * add background dot pattern css * add promise utils * init url based routing * update auth route server path as effect * add auth server hook * always use server from discovery info in context * login - WIP * upgrade jotai to v2 * add atom with localStorage util * add multi account sessions atom * add default IGNORE res to auto discovery * add error type in async callback hook * handle password login error * fix async callback hook * allow password login * Show custom server not allowed error in mxId login * add sso login component * add token login * fix hardcoded m.login.password in login func * update server input on url change * Improve sso login labels * update folds * fix async callback batching state update in safari * wrap async callback set state in queueMicrotask * wip * wip - register * arrange auth file structure * add error codes * extract filed error component form password login * add register util function * handle register flow - WIP * update unsupported auth flow method reasons * improve password input styles * Improve UIA flow next stage calculation complete stages can have any order so we will look for first stage which is not in completed * process register UIA flow stages * Extract register UIA stages component * improve register error messages * add focus trap & step count in UIA stages * add reset password path and path utils * add path with origin hook * fix sso redirect url * rename register token query param to token * restyle auth screen header * add reset password component - WIP * add reset password form * add netlify rewrites * fix netlify file indentation * test netlify redirect * fix vite to include netlify toml * add more netlify redirects * add splat to public and assets path * fix vite base name * add option to use hash router in config and remove appVersion * add splash screen component * add client config loading and error screen * fix server picker bug * fix reset password email input type * make auth page small screen responsive * fix typo in reset password screen
This commit is contained in:
parent
bb88eb7154
commit
20db27fa7e
103 changed files with 4772 additions and 543 deletions
274
src/app/pages/auth/reset-password/PasswordResetForm.tsx
Normal file
274
src/app/pages/auth/reset-password/PasswordResetForm.tsx
Normal file
|
|
@ -0,0 +1,274 @@
|
|||
import React, { FormEventHandler, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Dialog,
|
||||
Input,
|
||||
Overlay,
|
||||
OverlayBackdrop,
|
||||
OverlayCenter,
|
||||
Spinner,
|
||||
Text,
|
||||
color,
|
||||
config,
|
||||
} from 'folds';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import FocusTrap from 'focus-trap-react';
|
||||
import { AuthDict, AuthType, MatrixError, createClient } from 'matrix-js-sdk';
|
||||
import { useAutoDiscoveryInfo } from '../../../hooks/useAutoDiscoveryInfo';
|
||||
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
|
||||
import { useAuthServer } from '../../../hooks/useAuthServer';
|
||||
import { usePasswordEmail } from '../../../hooks/usePasswordEmail';
|
||||
import { PasswordInput } from '../../../components/password-input/PasswordInput';
|
||||
import { ConfirmPasswordMatch } from '../../../components/ConfirmPasswordMatch';
|
||||
import { FieldError } from '../FiledError';
|
||||
import { UIAFlowOverlay } from '../../../components/UIAFlowOverlay';
|
||||
import { EmailStageDialog } from '../../../components/uia-stages';
|
||||
import { ResetPasswordResult, resetPassword } from './resetPasswordUtil';
|
||||
import { getLoginPath, withSearchParam } from '../../pathUtils';
|
||||
import { LoginPathSearchParams } from '../../paths';
|
||||
import { getUIAError, getUIAErrorCode } from '../../../utils/matrix-uia';
|
||||
|
||||
type FormData = {
|
||||
email: string;
|
||||
password: string;
|
||||
clientSecret: string;
|
||||
};
|
||||
|
||||
function ResetPasswordComplete({ email }: { email?: string }) {
|
||||
const server = useAuthServer();
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleClick = () => {
|
||||
const path = getLoginPath(server);
|
||||
if (email) {
|
||||
navigate(withSearchParam<LoginPathSearchParams>(path, { email }));
|
||||
return;
|
||||
}
|
||||
navigate(path);
|
||||
};
|
||||
|
||||
return (
|
||||
<Overlay open backdrop={<OverlayBackdrop />}>
|
||||
<OverlayCenter>
|
||||
<FocusTrap>
|
||||
<Dialog>
|
||||
<Box style={{ padding: config.space.S400 }} direction="Column" gap="400">
|
||||
<Text>
|
||||
Password has been reset successfully. Please login with your new password.
|
||||
</Text>
|
||||
<Button variant="Primary" onClick={handleClick}>
|
||||
<Text size="B400" as="span">
|
||||
Login
|
||||
</Text>
|
||||
</Button>
|
||||
</Box>
|
||||
</Dialog>
|
||||
</FocusTrap>
|
||||
</OverlayCenter>
|
||||
</Overlay>
|
||||
);
|
||||
}
|
||||
|
||||
type PasswordResetFormProps = {
|
||||
defaultEmail?: string;
|
||||
};
|
||||
export function PasswordResetForm({ defaultEmail }: PasswordResetFormProps) {
|
||||
const server = useAuthServer();
|
||||
|
||||
const serverDiscovery = useAutoDiscoveryInfo();
|
||||
const baseUrl = serverDiscovery['m.homeserver'].base_url;
|
||||
const mx = useMemo(() => createClient({ baseUrl }), [baseUrl]);
|
||||
|
||||
const [formData, setFormData] = useState<FormData>();
|
||||
|
||||
const [passwordEmailState, passwordEmail] = usePasswordEmail(mx);
|
||||
|
||||
const [resetPasswordState, handleResetPassword] = useAsyncCallback<
|
||||
ResetPasswordResult,
|
||||
MatrixError,
|
||||
[AuthDict, string]
|
||||
>(useCallback(async (authDict, newPassword) => resetPassword(mx, authDict, newPassword), [mx]));
|
||||
|
||||
const [ongoingAuthData, resetPasswordResult] =
|
||||
resetPasswordState.status === AsyncStatus.Success ? resetPasswordState.data : [];
|
||||
const resetPasswordError =
|
||||
resetPasswordState.status === AsyncStatus.Error ? resetPasswordState.error : undefined;
|
||||
|
||||
const flowErrorCode = ongoingAuthData && getUIAErrorCode(ongoingAuthData);
|
||||
const flowError = ongoingAuthData && getUIAError(ongoingAuthData);
|
||||
|
||||
let waitingToVerifyEmail = true;
|
||||
if (resetPasswordResult) waitingToVerifyEmail = false;
|
||||
if (ongoingAuthData && flowErrorCode === undefined) waitingToVerifyEmail = false;
|
||||
if (resetPasswordError) waitingToVerifyEmail = false;
|
||||
if (resetPasswordState.status === AsyncStatus.Loading) waitingToVerifyEmail = false;
|
||||
|
||||
// We only support UIA m.login.password stage for reset password
|
||||
// So we will assume to process it as soon as
|
||||
// we have 401 with no error on initial request.
|
||||
useEffect(() => {
|
||||
if (formData && ongoingAuthData && !flowErrorCode) {
|
||||
handleResetPassword(
|
||||
{
|
||||
type: AuthType.Password,
|
||||
identifier: {
|
||||
type: 'm.id.thirdparty',
|
||||
medium: 'email',
|
||||
address: formData.email,
|
||||
},
|
||||
password: formData.password,
|
||||
},
|
||||
formData.password
|
||||
);
|
||||
}
|
||||
}, [ongoingAuthData, flowErrorCode, formData, handleResetPassword]);
|
||||
|
||||
const handleSubmit: FormEventHandler<HTMLFormElement> = (evt) => {
|
||||
evt.preventDefault();
|
||||
const { emailInput, passwordInput, confirmPasswordInput } = evt.target as HTMLFormElement & {
|
||||
emailInput: HTMLInputElement;
|
||||
passwordInput: HTMLInputElement;
|
||||
confirmPasswordInput: HTMLInputElement;
|
||||
};
|
||||
|
||||
const email = emailInput.value.trim();
|
||||
const password = passwordInput.value;
|
||||
const confirmPassword = confirmPasswordInput.value;
|
||||
if (!email) {
|
||||
emailInput.focus();
|
||||
return;
|
||||
}
|
||||
if (password !== confirmPassword) return;
|
||||
|
||||
const clientSecret = mx.generateClientSecret();
|
||||
passwordEmail(email, clientSecret);
|
||||
setFormData({
|
||||
email,
|
||||
password,
|
||||
clientSecret,
|
||||
});
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
const handleSubmitRequest = useCallback(
|
||||
(authDict: AuthDict) => {
|
||||
if (!formData) return;
|
||||
const { password } = formData;
|
||||
handleResetPassword(authDict, password);
|
||||
},
|
||||
[formData, handleResetPassword]
|
||||
);
|
||||
|
||||
return (
|
||||
<Box as="form" onSubmit={handleSubmit} direction="Inherit" gap="400">
|
||||
<Text size="T300" priority="400">
|
||||
Homeserver <strong>{server}</strong> will send you an email to let you reset your password.
|
||||
</Text>
|
||||
<Box direction="Column" gap="100">
|
||||
<Text as="label" size="L400" priority="300">
|
||||
Email
|
||||
</Text>
|
||||
<Input
|
||||
defaultValue={defaultEmail}
|
||||
type="email"
|
||||
name="emailInput"
|
||||
variant="Background"
|
||||
size="500"
|
||||
required
|
||||
outlined
|
||||
/>
|
||||
{passwordEmailState.status === AsyncStatus.Error && (
|
||||
<FieldError
|
||||
message={`${passwordEmailState.error.errcode}: ${passwordEmailState.error.data?.error}`}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
<ConfirmPasswordMatch initialValue>
|
||||
{(match, doMatch, passRef, confPassRef) => (
|
||||
<>
|
||||
<Box direction="Column" gap="100">
|
||||
<Text as="label" size="L400" priority="300">
|
||||
New Password
|
||||
</Text>
|
||||
<PasswordInput
|
||||
ref={passRef}
|
||||
onChange={doMatch}
|
||||
name="passwordInput"
|
||||
variant="Background"
|
||||
size="500"
|
||||
outlined
|
||||
required
|
||||
/>
|
||||
</Box>
|
||||
<Box direction="Column" gap="100">
|
||||
<Text as="label" size="L400" priority="300">
|
||||
Confirm Password
|
||||
</Text>
|
||||
<PasswordInput
|
||||
ref={confPassRef}
|
||||
onChange={doMatch}
|
||||
name="confirmPasswordInput"
|
||||
variant="Background"
|
||||
size="500"
|
||||
style={{ color: match ? undefined : color.Critical.Main }}
|
||||
outlined
|
||||
required
|
||||
/>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</ConfirmPasswordMatch>
|
||||
{resetPasswordError && (
|
||||
<FieldError
|
||||
message={`${resetPasswordError.errcode}: ${
|
||||
resetPasswordError.data?.error ?? 'Failed to reset password.'
|
||||
}`}
|
||||
/>
|
||||
)}
|
||||
<span data-spacing-node />
|
||||
<Button type="submit" variant="Primary" size="500">
|
||||
<Text as="span" size="B500">
|
||||
Reset Password
|
||||
</Text>
|
||||
</Button>
|
||||
|
||||
{resetPasswordResult && <ResetPasswordComplete email={formData?.email} />}
|
||||
|
||||
{passwordEmailState.status === AsyncStatus.Success && formData && waitingToVerifyEmail && (
|
||||
<UIAFlowOverlay currentStep={1} stepCount={1} onCancel={handleCancel}>
|
||||
<EmailStageDialog
|
||||
stageData={{
|
||||
type: AuthType.Email,
|
||||
errorCode: flowErrorCode,
|
||||
error: flowError,
|
||||
session: ongoingAuthData?.session,
|
||||
}}
|
||||
submitAuthDict={handleSubmitRequest}
|
||||
email={formData.email}
|
||||
clientSecret={formData.clientSecret}
|
||||
requestEmailToken={passwordEmail}
|
||||
emailTokenState={passwordEmailState}
|
||||
onCancel={handleCancel}
|
||||
/>
|
||||
</UIAFlowOverlay>
|
||||
)}
|
||||
|
||||
<Overlay
|
||||
open={
|
||||
passwordEmailState.status === AsyncStatus.Loading ||
|
||||
resetPasswordState.status === AsyncStatus.Loading
|
||||
}
|
||||
backdrop={<OverlayBackdrop />}
|
||||
>
|
||||
<OverlayCenter>
|
||||
<Spinner variant="Secondary" size="600" />
|
||||
</OverlayCenter>
|
||||
</Overlay>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
36
src/app/pages/auth/reset-password/ResetPassword.tsx
Normal file
36
src/app/pages/auth/reset-password/ResetPassword.tsx
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import { Box, Text } from 'folds';
|
||||
import React from 'react';
|
||||
import { Link, useSearchParams } from 'react-router-dom';
|
||||
import { getLoginPath } from '../../pathUtils';
|
||||
import { useAuthServer } from '../../../hooks/useAuthServer';
|
||||
import { PasswordResetForm } from './PasswordResetForm';
|
||||
|
||||
export type ResetPasswordSearchParams = {
|
||||
email?: string;
|
||||
};
|
||||
|
||||
const getResetPasswordSearchParams = (
|
||||
searchParams: URLSearchParams
|
||||
): ResetPasswordSearchParams => ({
|
||||
email: searchParams.get('email') ?? undefined,
|
||||
});
|
||||
|
||||
export function ResetPassword() {
|
||||
const server = useAuthServer();
|
||||
const [searchParams] = useSearchParams();
|
||||
const resetPasswordSearchParams = getResetPasswordSearchParams(searchParams);
|
||||
|
||||
return (
|
||||
<Box direction="Column" gap="500">
|
||||
<Text size="H2" priority="400">
|
||||
Reset Password
|
||||
</Text>
|
||||
<PasswordResetForm defaultEmail={resetPasswordSearchParams.email} />
|
||||
<span data-spacing-node />
|
||||
|
||||
<Text align="Center">
|
||||
Remember your password? <Link to={getLoginPath(server)}>Login</Link>
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
1
src/app/pages/auth/reset-password/index.ts
Normal file
1
src/app/pages/auth/reset-password/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from './ResetPassword';
|
||||
23
src/app/pages/auth/reset-password/resetPasswordUtil.ts
Normal file
23
src/app/pages/auth/reset-password/resetPasswordUtil.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import to from 'await-to-js';
|
||||
import { AuthDict, IAuthData, MatrixClient, MatrixError } from 'matrix-js-sdk';
|
||||
|
||||
export type ResetPasswordResponse = Record<string, never>;
|
||||
export type ResetPasswordResult = [IAuthData, undefined] | [undefined, ResetPasswordResponse];
|
||||
export const resetPassword = async (
|
||||
mx: MatrixClient,
|
||||
authDict: AuthDict,
|
||||
newPassword: string
|
||||
): Promise<ResetPasswordResult> => {
|
||||
const [err, res] = await to<ResetPasswordResponse, MatrixError>(
|
||||
mx.setPassword(authDict, newPassword, false)
|
||||
);
|
||||
|
||||
if (err) {
|
||||
if (err.httpStatus === 401) {
|
||||
const authData = err.data as IAuthData;
|
||||
return [authData, undefined];
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
return [undefined, res];
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue