Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
ajbura committed Jan 14, 2024
1 parent 7faccca commit 9741975
Show file tree
Hide file tree
Showing 13 changed files with 651 additions and 137 deletions.
38 changes: 10 additions & 28 deletions src/app/components/AuthFlowsLoader.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import { ReactNode, useCallback, useEffect, useMemo } from 'react';
import { IAuthData, MatrixError, createClient } from 'matrix-js-sdk';
import { MatrixError, createClient } from 'matrix-js-sdk';
import { AsyncStatus, useAsyncCallback } from '../hooks/useAsyncCallback';
import { useAutoDiscoveryInfo } from '../hooks/useAutoDiscoveryInfo';
import { promiseFulfilledResult, promiseRejectedResult } from '../utils/common';
import { AuthFlows, RegisterFlowStatus, RegisterFlowsResponse } from '../hooks/useAuthFlows';
import {
AuthFlows,
RegisterFlowStatus,
RegisterFlowsResponse,
parseRegisterErrResp,
} from '../hooks/useAuthFlows';

type AuthFlowsLoaderProps = {
fallback?: () => ReactNode;
Expand All @@ -20,34 +25,11 @@ export function AuthFlowsLoader({ fallback, error, children }: AuthFlowsLoaderPr
useCallback(async () => {
const result = await Promise.allSettled([mx.loginFlows(), mx.registerRequest({})]);
const loginFlows = promiseFulfilledResult(result[0]);
const registerReason = promiseRejectedResult(result[1]) as MatrixError | undefined;
const registerResp = promiseRejectedResult(result[1]) as MatrixError | undefined;
let registerFlows: RegisterFlowsResponse = { status: RegisterFlowStatus.InvalidRequest };

if (typeof registerReason === 'object' && registerReason.httpStatus) {
switch (registerReason.httpStatus) {
case RegisterFlowStatus.InvalidRequest: {
registerFlows = { status: RegisterFlowStatus.InvalidRequest };
break;
}
case RegisterFlowStatus.RateLimited: {
registerFlows = { status: RegisterFlowStatus.RateLimited };
break;
}
case RegisterFlowStatus.RegistrationDisabled: {
registerFlows = { status: RegisterFlowStatus.RegistrationDisabled };
break;
}
case RegisterFlowStatus.FlowRequired: {
registerFlows = {
status: RegisterFlowStatus.FlowRequired,
data: registerReason.data as IAuthData,
};
break;
}
default: {
registerFlows = { status: RegisterFlowStatus.InvalidRequest };
}
}
if (typeof registerResp === 'object' && registerResp.httpStatus) {
registerFlows = parseRegisterErrResp(registerResp);
}

if (!loginFlows) {
Expand Down
17 changes: 17 additions & 0 deletions src/app/components/SupportedUIAFlowsLoader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { ReactNode } from 'react';
import { UIAFlow } from 'matrix-js-sdk';
import { useSupportedUIAFlows } from '../hooks/useUIAFlows';

export function SupportedUIAFlowsLoader({
flows,
supportedStages,
children,
}: {
supportedStages: string[];
flows: UIAFlow[];
children: (supportedFlows: UIAFlow[]) => ReactNode;
}) {
const supportedFlows = useSupportedUIAFlows(flows, supportedStages);

return children(supportedFlows);
}
45 changes: 45 additions & 0 deletions src/app/components/password-input/PasswordInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import React, { ComponentProps, forwardRef } from 'react';
import { Icon, IconButton, Input, config, Icons } from 'folds';
import { UseStateProvider } from '../UseStateProvider';

type PasswordInputProps = Omit<ComponentProps<typeof Input>, 'type' | 'size'> & {
size: '400' | '500';
};
export const PasswordInput = forwardRef<HTMLInputElement, PasswordInputProps>(
({ variant, size, style, after, ...props }, ref) => {
const btnSize: ComponentProps<typeof IconButton>['size'] = size === '500' ? '400' : '300';

return (
<UseStateProvider initial={false}>
{(visible, setVisible) => (
<Input
{...props}
ref={ref}
style={{ paddingRight: config.space.S200, ...style }}
type={visible ? 'text' : 'password'}
size={size}
variant={visible ? 'Warning' : variant}
after={
<>
{after}
<IconButton
onClick={() => setVisible(!visible)}
type="button"
variant={visible ? 'Warning' : variant}
size={btnSize}
radii="300"
>
<Icon
style={{ opacity: config.opacity.P300 }}
size="100"
src={visible ? Icons.Eye : Icons.EyeBlind}
/>
</IconButton>
</>
}
/>
)}
</UseStateProvider>
);
}
);
25 changes: 24 additions & 1 deletion src/app/hooks/useAuthFlows.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { createContext, useContext } from 'react';
import { IAuthData } from 'matrix-js-sdk';
import { IAuthData, MatrixError } from 'matrix-js-sdk';
import { ILoginFlowsResponse } from 'matrix-js-sdk/lib/@types/auth';

export enum RegisterFlowStatus {
Expand All @@ -18,6 +18,29 @@ export type RegisterFlowsResponse =
status: Exclude<RegisterFlowStatus, RegisterFlowStatus.FlowRequired>;
};

export const parseRegisterErrResp = (matrixError: MatrixError): RegisterFlowsResponse => {
switch (matrixError.httpStatus) {
case RegisterFlowStatus.InvalidRequest: {
return { status: RegisterFlowStatus.InvalidRequest };
}
case RegisterFlowStatus.RateLimited: {
return { status: RegisterFlowStatus.RateLimited };
}
case RegisterFlowStatus.RegistrationDisabled: {
return { status: RegisterFlowStatus.RegistrationDisabled };
}
case RegisterFlowStatus.FlowRequired: {
return {
status: RegisterFlowStatus.FlowRequired,
data: matrixError.data as IAuthData,
};
}
default: {
return { status: RegisterFlowStatus.InvalidRequest };
}
}
};

export type AuthFlows = {
loginFlows: ILoginFlowsResponse;
registerFlows: RegisterFlowsResponse;
Expand Down
98 changes: 98 additions & 0 deletions src/app/hooks/useUIAFlows.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { AuthType, IAuthData, UIAFlow } from 'matrix-js-sdk';
import { useCallback, useMemo } from 'react';
import {
getSupportedUIAFlows,
getUIACompleted,
getUIAErrorCode,
getUIAParams,
getUIASession,
} from '../utils/matrix-uia';

export const SUPPORTED_FLOW_TYPES = [
AuthType.Dummy,
AuthType.Password,
AuthType.Email,
AuthType.Terms,
AuthType.Recaptcha,
AuthType.RegistrationToken,
] as const;

export const useSupportedUIAFlows = (uiaFlows: UIAFlow[], supportedStages: string[]): UIAFlow[] =>
useMemo(() => getSupportedUIAFlows(uiaFlows, supportedStages), [uiaFlows, supportedStages]);

export const useUIACompleted = (authData: IAuthData): string[] =>
useMemo(() => getUIACompleted(authData), [authData]);

export const useUIAParams = (authData: IAuthData) =>
useMemo(() => getUIAParams(authData), [authData]);

export const useUIASession = (authData: IAuthData) =>
useMemo(() => getUIASession(authData), [authData]);

export const useUIAErrorCode = (authData: IAuthData) =>
useMemo(() => getUIAErrorCode(authData), [authData]);

export type StageInfo = Record<string, unknown>;
export type AuthStageData = {
type: string;
info?: StageInfo;
session?: string;
errorCode?: string;
};
export type AuthStageDataGetter = () => AuthStageData | undefined;

export type UIAFlowInterface = {
getStageToComplete: AuthStageDataGetter;
hasStage: (stageType: string) => boolean;
getStageInfo: (stageType: string) => StageInfo | undefined;
};
export const useUIAFlow = (authData: IAuthData, uiaFlow: UIAFlow): UIAFlowInterface => {
const completed = useUIACompleted(authData);
const params = useUIAParams(authData);
const session = useUIASession(authData);
const errorCode = useUIAErrorCode(authData);

const getPrevCompletedStage = useCallback(() => {
const prevCompletedI = completed.length - 1;
const prevCompletedStage = prevCompletedI !== -1 ? completed[prevCompletedI] : undefined;
return prevCompletedStage;
}, [completed]);

const getStageToComplete: AuthStageDataGetter = useCallback(() => {
const { stages } = uiaFlow;
const prevCompletedStage = getPrevCompletedStage();

const nextStageIndex = stages.findIndex((stage) => stage === prevCompletedStage) + 1;
const nextStage = nextStageIndex < stages.length ? stages[nextStageIndex] : undefined;
if (!nextStage) return undefined;

const info = params[nextStage];

return {
type: nextStage,
info,
session,
errorCode,
};
}, [uiaFlow, getPrevCompletedStage, params, errorCode, session]);

const hasStage = useCallback(
(stageType: string): boolean => uiaFlow.stages.includes(stageType),
[uiaFlow]
);

const getStageInfo = useCallback(
(stageType: string): StageInfo | undefined => {
if (!hasStage(stageType)) return undefined;

return params[stageType];
},
[hasStage, params]
);

return {
getStageToComplete,
hasStage,
getStageInfo,
};
};
11 changes: 8 additions & 3 deletions src/app/pages/auth/AuthLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -114,11 +114,16 @@ export function AuthLayout() {

const selectServer = useCallback(
(newServer: string) => {
if (newServer === server) {
if (discoveryState.status === AsyncStatus.Loading) return;
discoverServer(server);
return;
}
navigate(
generatePath(currentAuthPath(location.pathname), { server: encodeURIComponent(newServer) })
);
},
[navigate, location]
[navigate, location, discoveryState, server, discoverServer]
);

const [autoDiscoveryError, autoDiscoveryInfo] =
Expand All @@ -131,7 +136,7 @@ export function AuthLayout() {
direction="Column"
alignItems="Center"
justifyContent="SpaceBetween"
gap="700"
gap="400"
>
<Box direction="Column" className={css.AuthCard}>
<Box justifyContent="Center">
Expand Down Expand Up @@ -182,7 +187,7 @@ export function AuthLayout() {
/>
)}
error={() => (
<AuthLayoutError message="Failed to connect. Homeserver URL does not appear to be a valid Matrix homeserver." />
<AuthLayoutError message="Failed to connect. Either homeserver is unavailable at this moment or does not exist." />
)}
>
{(specVersions) => (
Expand Down
6 changes: 5 additions & 1 deletion src/app/pages/auth/Login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export function Login() {
const { loginFlows } = useAuthFlows();
const [searchParams] = useSearchParams();
const loginSearchParams = getLoginSearchParams(searchParams);
const [ssoRedirectUrl] = window.location.href.split('?');

const parsedFlows = useParsedLoginFlows(loginFlows.flows);

Expand All @@ -52,7 +53,10 @@ export function Login() {
<>
<SSOLogin
providers={parsedFlows.sso.identity_providers}
asIcons={!!parsedFlows.password}
redirectUrl={ssoRedirectUrl}
asIcons={
parsedFlows.password !== undefined && parsedFlows.sso.identity_providers.length > 2
}
/>
<span data-spacing-node />
</>
Expand Down
32 changes: 2 additions & 30 deletions src/app/pages/auth/PasswordLoginForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import {
import FocusTrap from 'focus-trap-react';
import { Link, generatePath } from 'react-router-dom';
import { MatrixError } from 'matrix-js-sdk';
import { UseStateProvider } from '../../components/UseStateProvider';
import { getMxIdLocalPart, getMxIdServer, isUserId } from '../../utils/matrix';
import { EMAIL_REGEX } from '../../utils/regex';
import { useAutoDiscoveryInfo } from '../../hooks/useAutoDiscoveryInfo';
Expand All @@ -35,6 +34,7 @@ import {
login,
useLoginComplete,
} from './loginUtil';
import { PasswordInput } from '../../components/password-input/PasswordInput';

function UsernameHint({ server }: { server: string }) {
const [open, setOpen] = useState(false);
Expand Down Expand Up @@ -233,34 +233,7 @@ export function PasswordLoginForm({ defaultUsername, defaultEmail }: PasswordLog
<Text as="label" size="L400" priority="300">
Password
</Text>
<UseStateProvider initial={false}>
{(visible, setVisible) => (
<Input
style={{ paddingRight: config.space.S200 }}
name="passwordInput"
type={visible ? 'text' : 'password'}
variant={visible ? 'Warning' : 'Background'}
size="500"
outlined
required
after={
<IconButton
onClick={() => setVisible(!visible)}
type="button"
variant={visible ? 'Warning' : 'Background'}
size="400"
radii="300"
>
<Icon
style={{ opacity: config.opacity.P300 }}
size="100"
src={visible ? Icons.Eye : Icons.EyeBlind}
/>
</IconButton>
}
/>
)}
</UseStateProvider>
<PasswordInput name="passwordInput" variant="Background" size="500" outlined required />
<Box alignItems="Start" justifyContent="SpaceBetween" gap="200">
{loginState.status === AsyncStatus.Error && (
<>
Expand Down Expand Up @@ -289,7 +262,6 @@ export function PasswordLoginForm({ defaultUsername, defaultEmail }: PasswordLog
</Box>
</Box>
</Box>
<span />
<Button type="submit" variant="Primary" size="500">
<Text as="span" size="B500">
Login
Expand Down
Loading

0 comments on commit 9741975

Please sign in to comment.