feat: consistent authentication flow

fix: missing suspense on login
feat: re-prompt MFA if fail on login
This commit is contained in:
Paul Makles 2022-06-29 16:27:57 +01:00
parent 0261fec676
commit 1fcb3cedc1
5 changed files with 57 additions and 37 deletions

View file

@ -41,10 +41,9 @@ export default function Context({ children }: { children: Children }) {
<UIProvider value={uiContext}> <UIProvider value={uiContext}>
<Locale> <Locale>
<Intermediate> <Intermediate>
<Binder> {children}
{children} <SyncManager />
{<SyncManager />} <Binder />
</Binder>
</Intermediate> </Intermediate>
<ModalRenderer /> <ModalRenderer />
</Locale> </Locale>

View file

@ -1,6 +1,8 @@
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { Redirect } from "react-router-dom"; import { Redirect } from "react-router-dom";
import { Preloader } from "@revoltchat/ui";
import { clientController } from "../../controllers/client/ClientController"; import { clientController } from "../../controllers/client/ClientController";
interface Props { interface Props {
@ -10,6 +12,10 @@ interface Props {
children: Children; children: Children;
} }
/**
* Check that we are logged in or out and redirect accordingly.
* Also prevent render until the client is ready to display.
*/
export const CheckAuth = observer((props: Props) => { export const CheckAuth = observer((props: Props) => {
const loggedIn = clientController.isLoggedIn(); const loggedIn = clientController.isLoggedIn();
@ -22,5 +28,14 @@ export const CheckAuth = observer((props: Props) => {
return <Redirect to="/" />; return <Redirect to="/" />;
} }
// Block render if client is getting ready to work.
if (
props.auth &&
clientController.isLoggedIn() &&
!clientController.isReady()
) {
return <Preloader type="spinner" />;
}
return <>{props.children}</>; return <>{props.children}</>;
}); });

View file

@ -7,6 +7,7 @@ import { injectController } from "../../lib/window";
import { state } from "../../mobx/State"; import { state } from "../../mobx/State";
import Auth from "../../mobx/stores/Auth"; import Auth from "../../mobx/stores/Auth";
import { resetMemberSidebarFetched } from "../../components/navigation/right/MemberSidebar";
import { modalController } from "../modals/ModalController"; import { modalController } from "../modals/ModalController";
import Session from "./Session"; import Session from "./Session";
@ -205,29 +206,37 @@ class ClientController {
// Prompt for MFA verificaiton if necessary // Prompt for MFA verificaiton if necessary
if (session.result === "MFA") { if (session.result === "MFA") {
const { allowed_methods } = session; const { allowed_methods } = session;
const mfa_response: API.MFAResponse | undefined = await new Promise( while (session.result === "MFA") {
(callback) => const mfa_response: API.MFAResponse | undefined =
modalController.push({ await new Promise((callback) =>
type: "mfa_flow", modalController.push({
state: "unknown", type: "mfa_flow",
available_methods: allowed_methods, state: "unknown",
callback, available_methods: allowed_methods,
}), callback,
); }),
);
if (typeof mfa_response === "undefined") { if (typeof mfa_response === "undefined") {
throw "Cancelled"; break;
}
try {
session = await this.apiClient.api.post(
"/auth/session/login",
{
mfa_response,
mfa_ticket: session.ticket,
friendly_name,
},
);
} catch (err) {
console.error("Failed login:", err);
}
} }
session = await this.apiClient.api.post("/auth/session/login", {
mfa_response,
mfa_ticket: session.ticket,
friendly_name,
});
if (session.result === "MFA") { if (session.result === "MFA") {
// unreachable code throw "Cancelled";
return;
} }
} }
@ -247,12 +256,12 @@ class ClientController {
@action logout(user_id: string) { @action logout(user_id: string) {
const session = this.sessions.get(user_id); const session = this.sessions.get(user_id);
if (session) { if (session) {
this.sessions.delete(user_id);
if (user_id === this.current) { if (user_id === this.current) {
this.current = null; this.current = null;
this.pickNextSession();
} }
this.sessions.delete(user_id);
this.pickNextSession();
session.destroy(); session.destroy();
} }
} }
@ -272,6 +281,10 @@ class ClientController {
*/ */
@action switchAccount(user_id: string) { @action switchAccount(user_id: string) {
this.current = user_id; this.current = user_id;
// This will allow account switching to work more seamlessly,
// maybe it'll be properly / fully implemented at some point.
resetMemberSidebarFetched();
} }
} }

View file

@ -2,26 +2,17 @@ import { observer } from "mobx-react-lite";
import { useEffect } from "preact/hooks"; import { useEffect } from "preact/hooks";
import { Preloader } from "@revoltchat/ui";
import { state } from "../../../mobx/State"; import { state } from "../../../mobx/State";
import { clientController } from "../ClientController"; import { clientController } from "../ClientController";
/** /**
* Prevent render until the client is ready to display.
* Also binds listeners from state to the current client. * Also binds listeners from state to the current client.
*/ */
const Binder: React.FC = ({ children }) => { const Binder: React.FC = () => {
const client = clientController.getReadyClient(); const client = clientController.getReadyClient();
useEffect(() => state.registerListeners(client!), [client]); useEffect(() => state.registerListeners(client!), [client]);
return null;
// Block render if client is getting ready to work.
if (clientController.isLoggedIn() && !clientController.isReady()) {
return <Preloader type="spinner" />;
}
return <>{children}</>;
}; };
export default observer(Binder); export default observer(Binder);

View file

@ -49,7 +49,9 @@ export function App() {
</Route> </Route>
<Route path="/login"> <Route path="/login">
<CheckAuth> <CheckAuth>
<Login /> <LoadSuspense>
<Login />
</LoadSuspense>
</CheckAuth> </CheckAuth>
</Route> </Route>
<Route path="/"> <Route path="/">