mirror of
https://github.com/cinnyapp/cinny.git
synced 2025-11-13 18:50: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
272
src/app/pages/auth/login/PasswordLoginForm.tsx
Normal file
272
src/app/pages/auth/login/PasswordLoginForm.tsx
Normal file
|
|
@ -0,0 +1,272 @@
|
|||
import React, { FormEventHandler, useCallback, useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Header,
|
||||
Icon,
|
||||
IconButton,
|
||||
Icons,
|
||||
Input,
|
||||
Menu,
|
||||
Overlay,
|
||||
OverlayBackdrop,
|
||||
OverlayCenter,
|
||||
PopOut,
|
||||
Spinner,
|
||||
Text,
|
||||
config,
|
||||
} from 'folds';
|
||||
import FocusTrap from 'focus-trap-react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { MatrixError } from 'matrix-js-sdk';
|
||||
import { getMxIdLocalPart, getMxIdServer, isUserId } from '../../../utils/matrix';
|
||||
import { EMAIL_REGEX } from '../../../utils/regex';
|
||||
import { useAutoDiscoveryInfo } from '../../../hooks/useAutoDiscoveryInfo';
|
||||
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
|
||||
import { useAuthServer } from '../../../hooks/useAuthServer';
|
||||
import { useClientConfig } from '../../../hooks/useClientConfig';
|
||||
import {
|
||||
CustomLoginResponse,
|
||||
LoginError,
|
||||
factoryGetBaseUrl,
|
||||
login,
|
||||
useLoginComplete,
|
||||
} from './loginUtil';
|
||||
import { PasswordInput } from '../../../components/password-input/PasswordInput';
|
||||
import { FieldError } from '../FiledError';
|
||||
import { getResetPasswordPath } from '../../pathUtils';
|
||||
|
||||
function UsernameHint({ server }: { server: string }) {
|
||||
const [open, setOpen] = useState(false);
|
||||
return (
|
||||
<PopOut
|
||||
open={open}
|
||||
position="Top"
|
||||
align="End"
|
||||
content={
|
||||
<FocusTrap
|
||||
focusTrapOptions={{
|
||||
initialFocus: false,
|
||||
onDeactivate: () => setOpen(false),
|
||||
clickOutsideDeactivates: true,
|
||||
}}
|
||||
>
|
||||
<Menu>
|
||||
<Header size="300" style={{ padding: `0 ${config.space.S200}` }}>
|
||||
<Text size="L400">Hint</Text>
|
||||
</Header>
|
||||
<Box
|
||||
style={{ padding: config.space.S200, paddingTop: 0 }}
|
||||
direction="Column"
|
||||
tabIndex={0}
|
||||
gap="100"
|
||||
>
|
||||
<Text size="T300">
|
||||
<Text as="span" size="Inherit" priority="300">
|
||||
Username:
|
||||
</Text>{' '}
|
||||
johndoe
|
||||
</Text>
|
||||
<Text size="T300">
|
||||
<Text as="span" size="Inherit" priority="300">
|
||||
Matrix ID:
|
||||
</Text>
|
||||
{` @johndoe:${server}`}
|
||||
</Text>
|
||||
<Text size="T300">
|
||||
<Text as="span" size="Inherit" priority="300">
|
||||
Email:
|
||||
</Text>
|
||||
{` johndoe@${server}`}
|
||||
</Text>
|
||||
</Box>
|
||||
</Menu>
|
||||
</FocusTrap>
|
||||
}
|
||||
>
|
||||
{(targetRef) => (
|
||||
<IconButton
|
||||
tabIndex={-1}
|
||||
onClick={() => setOpen(true)}
|
||||
ref={targetRef}
|
||||
type="button"
|
||||
variant="Background"
|
||||
size="300"
|
||||
radii="300"
|
||||
aria-pressed={open}
|
||||
>
|
||||
<Icon style={{ opacity: config.opacity.P300 }} size="100" src={Icons.Info} />
|
||||
</IconButton>
|
||||
)}
|
||||
</PopOut>
|
||||
);
|
||||
}
|
||||
|
||||
type PasswordLoginFormProps = {
|
||||
defaultUsername?: string;
|
||||
defaultEmail?: string;
|
||||
};
|
||||
export function PasswordLoginForm({ defaultUsername, defaultEmail }: PasswordLoginFormProps) {
|
||||
const server = useAuthServer();
|
||||
const clientConfig = useClientConfig();
|
||||
|
||||
const serverDiscovery = useAutoDiscoveryInfo();
|
||||
const baseUrl = serverDiscovery['m.homeserver'].base_url;
|
||||
|
||||
const [loginState, startLogin] = useAsyncCallback<
|
||||
CustomLoginResponse,
|
||||
MatrixError,
|
||||
Parameters<typeof login>
|
||||
>(useCallback(login, []));
|
||||
|
||||
useLoginComplete(loginState.status === AsyncStatus.Success ? loginState.data : undefined);
|
||||
|
||||
const handleUsernameLogin = (username: string, password: string) => {
|
||||
startLogin(baseUrl, {
|
||||
type: 'm.login.password',
|
||||
identifier: {
|
||||
type: 'm.id.user',
|
||||
user: username,
|
||||
},
|
||||
password,
|
||||
initial_device_display_name: 'Cinny Web',
|
||||
});
|
||||
};
|
||||
|
||||
const handleMxIdLogin = async (mxId: string, password: string) => {
|
||||
const mxIdServer = getMxIdServer(mxId);
|
||||
const mxIdUsername = getMxIdLocalPart(mxId);
|
||||
if (!mxIdServer || !mxIdUsername) return;
|
||||
|
||||
const getBaseUrl = factoryGetBaseUrl(clientConfig, mxIdServer);
|
||||
|
||||
startLogin(getBaseUrl, {
|
||||
type: 'm.login.password',
|
||||
identifier: {
|
||||
type: 'm.id.user',
|
||||
user: mxIdUsername,
|
||||
},
|
||||
password,
|
||||
initial_device_display_name: 'Cinny Web',
|
||||
});
|
||||
};
|
||||
const handleEmailLogin = (email: string, password: string) => {
|
||||
startLogin(baseUrl, {
|
||||
type: 'm.login.password',
|
||||
identifier: {
|
||||
type: 'm.id.thirdparty',
|
||||
medium: 'email',
|
||||
address: email,
|
||||
},
|
||||
password,
|
||||
initial_device_display_name: 'Cinny Web',
|
||||
});
|
||||
};
|
||||
|
||||
const handleSubmit: FormEventHandler<HTMLFormElement> = (evt) => {
|
||||
evt.preventDefault();
|
||||
const { usernameInput, passwordInput } = evt.target as HTMLFormElement & {
|
||||
usernameInput: HTMLInputElement;
|
||||
passwordInput: HTMLInputElement;
|
||||
};
|
||||
|
||||
const username = usernameInput.value.trim();
|
||||
const password = passwordInput.value;
|
||||
if (!username) {
|
||||
usernameInput.focus();
|
||||
return;
|
||||
}
|
||||
if (!password) {
|
||||
passwordInput.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (isUserId(username)) {
|
||||
handleMxIdLogin(username, password);
|
||||
return;
|
||||
}
|
||||
if (EMAIL_REGEX.test(username)) {
|
||||
handleEmailLogin(username, password);
|
||||
return;
|
||||
}
|
||||
handleUsernameLogin(username, password);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box as="form" onSubmit={handleSubmit} direction="Inherit" gap="400">
|
||||
<Box direction="Column" gap="100">
|
||||
<Text as="label" size="L400" priority="300">
|
||||
Username
|
||||
</Text>
|
||||
<Input
|
||||
defaultValue={defaultUsername ?? defaultEmail}
|
||||
style={{ paddingRight: config.space.S300 }}
|
||||
name="usernameInput"
|
||||
variant="Background"
|
||||
size="500"
|
||||
required
|
||||
outlined
|
||||
after={<UsernameHint server={server} />}
|
||||
/>
|
||||
{loginState.status === AsyncStatus.Error && (
|
||||
<>
|
||||
{loginState.error.errcode === LoginError.ServerNotAllowed && (
|
||||
<FieldError message="Login with custom server not allowed by your client instance." />
|
||||
)}
|
||||
{loginState.error.errcode === LoginError.InvalidServer && (
|
||||
<FieldError message="Failed to find your Matrix ID server." />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
<Box direction="Column" gap="100">
|
||||
<Text as="label" size="L400" priority="300">
|
||||
Password
|
||||
</Text>
|
||||
<PasswordInput name="passwordInput" variant="Background" size="500" outlined required />
|
||||
<Box alignItems="Start" justifyContent="SpaceBetween" gap="200">
|
||||
{loginState.status === AsyncStatus.Error && (
|
||||
<>
|
||||
{loginState.error.errcode === LoginError.Forbidden && (
|
||||
<FieldError message="Invalid Username or Password." />
|
||||
)}
|
||||
{loginState.error.errcode === LoginError.UserDeactivated && (
|
||||
<FieldError message="This account has been deactivated." />
|
||||
)}
|
||||
{loginState.error.errcode === LoginError.InvalidRequest && (
|
||||
<FieldError message="Failed to login. Part of your request data is invalid." />
|
||||
)}
|
||||
{loginState.error.errcode === LoginError.RateLimited && (
|
||||
<FieldError message="Failed to login. Your login request has been rate-limited by server, Please try after some time." />
|
||||
)}
|
||||
{loginState.error.errcode === LoginError.Unknown && (
|
||||
<FieldError message="Failed to login. Unknown reason." />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<Box grow="Yes" shrink="No" justifyContent="End">
|
||||
<Text as="span" size="T200" priority="400" align="Right">
|
||||
<Link to={getResetPasswordPath(server)}>Forget Password?</Link>
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
<Button type="submit" variant="Primary" size="500">
|
||||
<Text as="span" size="B500">
|
||||
Login
|
||||
</Text>
|
||||
</Button>
|
||||
|
||||
<Overlay
|
||||
open={
|
||||
loginState.status === AsyncStatus.Loading || loginState.status === AsyncStatus.Success
|
||||
}
|
||||
backdrop={<OverlayBackdrop />}
|
||||
>
|
||||
<OverlayCenter>
|
||||
<Spinner variant="Secondary" size="600" />
|
||||
</OverlayCenter>
|
||||
</Overlay>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue