Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Auth - URL Navigation #1603

Merged
merged 70 commits into from
Jan 21, 2024
Merged
Show file tree
Hide file tree
Changes from 31 commits
Commits
Show all changes
70 commits
Select commit Hold shift + click to select a range
24064a6
bump to react 18 and install react-router-dom
ajbura Nov 29, 2023
15d5123
Upgrade to react 18 root
ajbura Dec 4, 2023
5bb47c2
update vite
ajbura Dec 20, 2023
aa714a2
add cs api's
ajbura Dec 20, 2023
6300ef8
convert state/auth to ts
ajbura Dec 20, 2023
ab4aabb
add client config context
ajbura Dec 20, 2023
fed6916
add auto discovery context
ajbura Dec 20, 2023
04c8bfd
add spec version context
ajbura Dec 20, 2023
18757f6
add auth flow context
ajbura Dec 20, 2023
55cbff0
add background dot pattern css
ajbura Dec 20, 2023
b51b2cc
add promise utils
ajbura Dec 20, 2023
7f2d09e
init url based routing
ajbura Dec 20, 2023
12c4269
update auth route server path as effect
ajbura Dec 20, 2023
c7a1600
add auth server hook
ajbura Dec 20, 2023
dab1747
always use server from discovery info in context
ajbura Dec 20, 2023
d936c93
Merge branch 'dev' into refactor-navigation
ajbura Dec 21, 2023
1d7f027
login - WIP
ajbura Dec 23, 2023
bd7564a
upgrade jotai to v2
ajbura Dec 24, 2023
1e449c6
add atom with localStorage util
ajbura Dec 24, 2023
b049e58
add multi account sessions atom
ajbura Dec 24, 2023
fd91be2
Merge branch 'dev' into refactor-navigation
ajbura Dec 24, 2023
88c50cc
add default IGNORE res to auto discovery
ajbura Dec 25, 2023
f397b4b
add error type in async callback hook
ajbura Dec 25, 2023
041fa52
handle password login error
ajbura Dec 25, 2023
fbb690c
fix async callback hook
ajbura Dec 25, 2023
70797a8
allow password login
ajbura Dec 25, 2023
f339cab
Show custom server not allowed error in mxId login
ajbura Dec 26, 2023
6bb9365
add sso login component
ajbura Dec 26, 2023
920da5b
add token login
ajbura Dec 26, 2023
a1f6189
fix hardcoded m.login.password in login func
ajbura Dec 26, 2023
a5f2cb6
update server input on url change
ajbura Dec 26, 2023
9c039eb
Improve sso login labels
ajbura Dec 27, 2023
d214dfc
update folds
ajbura Dec 30, 2023
5dcf450
fix async callback batching state update in safari
ajbura Dec 30, 2023
7faccca
wrap async callback set state in queueMicrotask
ajbura Dec 31, 2023
9741975
wip
ajbura Jan 14, 2024
6782bf0
wip - register
ajbura Jan 14, 2024
bdb7fbc
arrange auth file structure
ajbura Jan 14, 2024
77b62db
add error codes
ajbura Jan 15, 2024
39d65bc
extract filed error component form password login
ajbura Jan 15, 2024
2265c47
add register util function
ajbura Jan 15, 2024
f142f61
handle register flow - WIP
ajbura Jan 15, 2024
2f1d278
update unsupported auth flow method reasons
ajbura Jan 15, 2024
a3b36c5
improve password input styles
ajbura Jan 17, 2024
5543a54
Improve UIA flow next stage calculation
ajbura Jan 17, 2024
a3b1878
process register UIA flow stages
ajbura Jan 17, 2024
3c7e3b1
Extract register UIA stages component
ajbura Jan 17, 2024
22eb46e
improve register error messages
ajbura Jan 18, 2024
07f00fd
add focus trap & step count in UIA stages
ajbura Jan 18, 2024
8a7cd53
add reset password path and path utils
ajbura Jan 18, 2024
0627bb4
add path with origin hook
ajbura Jan 18, 2024
40171ab
fix sso redirect url
ajbura Jan 18, 2024
d55ef89
rename register token query param to token
ajbura Jan 18, 2024
42b5a7f
restyle auth screen header
ajbura Jan 18, 2024
66ae076
add reset password component - WIP
ajbura Jan 18, 2024
3372df6
add reset password form
ajbura Jan 19, 2024
a6dc96a
add netlify rewrites
ajbura Jan 20, 2024
aacbcc8
fix netlify file indentation
ajbura Jan 20, 2024
a66c31e
test netlify redirect
ajbura Jan 20, 2024
c2c8c02
fix vite to include netlify toml
ajbura Jan 20, 2024
c7d90f1
add more netlify redirects
ajbura Jan 20, 2024
3a04616
add splat to public and assets path
ajbura Jan 20, 2024
fdf2521
fix vite base name
ajbura Jan 20, 2024
845eebe
add option to use hash router in config and remove appVersion
ajbura Jan 20, 2024
3a08785
add splash screen component
ajbura Jan 20, 2024
59483cf
add client config loading and error screen
ajbura Jan 20, 2024
5da9043
fix server picker bug
ajbura Jan 21, 2024
8a8d7d4
fix reset password email input type
ajbura Jan 21, 2024
a1fc66b
make auth page small screen responsive
ajbura Jan 21, 2024
f9d0407
fix typo in reset password screen
ajbura Jan 21, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions config.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
{
"appVersion": "3.2.0",
ajbura marked this conversation as resolved.
Show resolved Hide resolved
"basename": "/",
"defaultHomeserver": 2,
"homeserverList": [
"converser.eu",
Expand Down
2 changes: 1 addition & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,6 @@
<audio id="inviteSound">
<source src="./public/sound/invite.ogg" type="audio/ogg" />
</audio>
<script type="module" src="./src/index.jsx"></script>
<script type="module" src="./src/index.tsx"></script>
</body>
</html>
1,006 changes: 625 additions & 381 deletions package-lock.json

Large diffs are not rendered by default.

19 changes: 10 additions & 9 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
"html-react-parser": "4.2.0",
"immer": "9.0.16",
"is-hotkey": "0.2.0",
"jotai": "1.12.0",
"jotai": "2.6.0",
"katex": "0.16.4",
"linkify-html": "4.0.2",
"linkify-react": "4.1.1",
Expand All @@ -55,17 +55,18 @@
"pdfjs-dist": "3.10.111",
"prismjs": "1.29.0",
"prop-types": "15.8.1",
"react": "17.0.2",
"react": "18.2.0",
"react-aria": "3.29.1",
"react-autosize-textarea": "7.1.0",
"react-blurhash": "0.2.0",
"react-dnd": "15.1.2",
"react-dnd-html5-backend": "15.1.3",
"react-dom": "17.0.2",
"react-dnd": "16.0.1",
"react-dnd-html5-backend": "16.0.1",
"react-dom": "18.2.0",
"react-error-boundary": "4.0.10",
"react-google-recaptcha": "2.1.0",
"react-modal": "3.16.1",
"react-range": "1.8.14",
"react-router-dom": "6.20.0",
"sanitize-html": "2.8.0",
"slate": "0.94.1",
"slate-history": "0.93.0",
Expand All @@ -81,13 +82,13 @@
"@types/file-saver": "2.0.5",
"@types/node": "18.11.18",
"@types/prismjs": "1.26.0",
"@types/react": "18.0.26",
"@types/react-dom": "18.0.9",
"@types/react": "18.2.39",
"@types/react-dom": "18.2.17",
"@types/sanitize-html": "2.9.0",
"@types/ua-parser-js": "0.7.36",
"@typescript-eslint/eslint-plugin": "5.46.1",
"@typescript-eslint/parser": "5.46.1",
"@vitejs/plugin-react": "3.0.0",
"@vitejs/plugin-react": "4.2.0",
"buffer": "6.0.3",
"eslint": "8.29.0",
"eslint-config-airbnb": "19.0.4",
Expand All @@ -100,7 +101,7 @@
"prettier": "2.8.1",
"sass": "1.56.2",
"typescript": "4.9.4",
"vite": "4.3.9",
"vite": "5.0.8",
"vite-plugin-static-copy": "0.13.0"
}
}
80 changes: 80 additions & 0 deletions src/app/components/AuthFlowsLoader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { ReactNode, useCallback, useMemo } from 'react';
import { IAuthData, 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';

type AuthFlowsLoaderProps = {
fallback?: () => ReactNode;
error?: (err: unknown) => ReactNode;
children: (versions: AuthFlows) => ReactNode;
};
export function AuthFlowsLoader({ fallback, error, children }: AuthFlowsLoaderProps) {
const autoDiscoveryInfo = useAutoDiscoveryInfo();
const baseUrl = autoDiscoveryInfo['m.homeserver'].base_url;

const mx = useMemo(() => createClient({ baseUrl }), [baseUrl]);

const [state, load] = useAsyncCallback(
useCallback(async () => {
const result = await Promise.allSettled([mx.loginFlows(), mx.registerRequest({})]);
const loginFlows = promiseFulfilledResult(result[0]);
const registerReason = 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 (!loginFlows) {
throw new Error('Missing auth flow!');
}
if ('errcode' in loginFlows) {
throw new Error('Failed to load auth flow!');
}

const authFlows: AuthFlows = {
loginFlows,
registerFlows,
};

return authFlows;
}, [mx])
);

if (state.status === AsyncStatus.Idle) load();

if (state.status === AsyncStatus.Idle || state.status === AsyncStatus.Loading) {
return fallback?.();
}

if (state.status === AsyncStatus.Error) {
return error?.(state.error);
}

return children(state.data);
}
26 changes: 26 additions & 0 deletions src/app/components/ClientConfigLoader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { ReactNode } from 'react';
import { AsyncStatus, useAsyncCallback } from '../hooks/useAsyncCallback';
import { ClientConfig } from '../hooks/useClientConfig';

const getClientConfig = async (): Promise<ClientConfig> => {
const config = await fetch('/config.json', { method: 'GET' });
return config.json();
};

type ClientConfigLoaderProps = {
fallback?: () => ReactNode;
children: (config: ClientConfig) => ReactNode;
};
export function ClientConfigLoader({ fallback, children }: ClientConfigLoaderProps) {
const [state, load] = useAsyncCallback(getClientConfig);

if (state.status === AsyncStatus.Idle) load();

if (state.status === AsyncStatus.Idle || state.status === AsyncStatus.Loading) {
return fallback?.();
}

const config: ClientConfig = state.status === AsyncStatus.Success ? state.data : {};

return children(config);
}
30 changes: 30 additions & 0 deletions src/app/components/SpecVersionsLoader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { ReactNode, useCallback } from 'react';
import { AsyncStatus, useAsyncCallback } from '../hooks/useAsyncCallback';
import { SpecVersions, specVersions } from '../cs-api';
import { useAutoDiscoveryInfo } from '../hooks/useAutoDiscoveryInfo';

type SpecVersionsLoaderProps = {
fallback?: () => ReactNode;
error?: (err: unknown) => ReactNode;
children: (versions: SpecVersions) => ReactNode;
};
export function SpecVersionsLoader({ fallback, error, children }: SpecVersionsLoaderProps) {
const autoDiscoveryInfo = useAutoDiscoveryInfo();
const baseUrl = autoDiscoveryInfo['m.homeserver'].base_url;

const [state, load] = useAsyncCallback(
useCallback(() => specVersions(fetch, baseUrl), [baseUrl])
);

if (state.status === AsyncStatus.Idle) load();

if (state.status === AsyncStatus.Idle || state.status === AsyncStatus.Loading) {
return fallback?.();
}

if (state.status === AsyncStatus.Error) {
return error?.(state.error);
}

return children(state.data);
}
115 changes: 115 additions & 0 deletions src/app/cs-api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import to from 'await-to-js';
import { trimTrailingSlash } from './utils/common';

export enum AutoDiscoveryAction {
PROMPT = 'PROMPT',
IGNORE = 'IGNORE',
FAIL_PROMPT = 'FAIL_PROMPT',
FAIL_ERROR = 'FAIL_ERROR',
}

export type AutoDiscoveryError = {
host: string;
action: AutoDiscoveryAction;
};

export type AutoDiscoveryInfo = Record<string, unknown> & {
'm.homeserver': {
base_url: string;
};
'm.identity_server'?: {
base_url: string;
};
};

export const autoDiscovery = async (
request: typeof fetch,
server: string
): Promise<[AutoDiscoveryError, undefined] | [undefined, AutoDiscoveryInfo]> => {
const host = /^https?:\/\//.test(server) ? trimTrailingSlash(server) : `https://${server}`;
const autoDiscoveryUrl = `${host}/.well-known/matrix/client`;

const [err, response] = await to(request(autoDiscoveryUrl, { method: 'GET' }));

if (err || response.status === 404) {
// AutoDiscoveryAction.IGNORE
// We will use default value for IGNORE action
return [
undefined,
{
'm.homeserver': {
base_url: host,
},
},
];
}
if (response.status !== 200) {
return [
{
host,
action: AutoDiscoveryAction.FAIL_PROMPT,
},
undefined,
];
}

const [contentErr, content] = await to<AutoDiscoveryInfo>(response.json());

if (contentErr || typeof content !== 'object') {
return [
{
host,
action: AutoDiscoveryAction.FAIL_PROMPT,
},
undefined,
];
}

const baseUrl = content['m.homeserver']?.base_url;
if (typeof baseUrl !== 'string') {
return [
{
host,
action: AutoDiscoveryAction.FAIL_PROMPT,
},
undefined,
];
}

if (/^https?:\/\//.test(baseUrl) === false) {
return [
{
host,
action: AutoDiscoveryAction.FAIL_ERROR,
},
undefined,
];
}

content['m.homeserver'].base_url = trimTrailingSlash(baseUrl);
if (content['m.identity_server']) {
content['m.identity_server'].base_url = trimTrailingSlash(
content['m.identity_server'].base_url
);
}

return [undefined, content];
};

export type SpecVersions = {
versions: string[];
unstable_features?: Record<string, boolean>;
};
export const specVersions = async (
request: typeof fetch,
baseUrl: string
): Promise<SpecVersions> => {
const res = await request(`${baseUrl}/_matrix/client/versions`);

const data = (await res.json()) as unknown;

if (data && typeof data === 'object' && 'versions' in data && Array.isArray(data.versions)) {
return data as SpecVersions;
}
throw new Error('Homeserver URL does not appear to be a valid Matrix homeserver');
};
Loading