From 66ae518e51464fa43f9db23322c4a5201a9ce118 Mon Sep 17 00:00:00 2001 From: Paul Makles Date: Wed, 29 Jun 2022 10:52:42 +0100 Subject: [PATCH] feat: make login functional again --- src/controllers/client/ClientController.tsx | 162 +++++++++++++++++--- src/controllers/client/Session.tsx | 9 +- src/mobx/State.ts | 4 +- src/pages/login/forms/CaptchaBlock.tsx | 4 +- src/pages/login/forms/Form.tsx | 11 +- src/pages/login/forms/FormCreate.tsx | 6 +- src/pages/login/forms/FormLogin.tsx | 104 +------------ 7 files changed, 160 insertions(+), 140 deletions(-) diff --git a/src/controllers/client/ClientController.tsx b/src/controllers/client/ClientController.tsx index be9497ce..9939df17 100644 --- a/src/controllers/client/ClientController.tsx +++ b/src/controllers/client/ClientController.tsx @@ -1,8 +1,10 @@ +import { detect } from "detect-browser"; import { action, computed, makeAutoObservable, ObservableMap } from "mobx"; -import { Client, Nullable } from "revolt.js"; +import { API, Client, Nullable } from "revolt.js"; import { injectController } from "../../lib/window"; +import { state } from "../../mobx/State"; import Auth from "../../mobx/stores/Auth"; import { modalController } from "../modals/ModalController"; @@ -14,6 +16,11 @@ class ClientController { */ private apiClient: Client; + /** + * Server configuration + */ + private configuration: API.RevoltConfig | null; + /** * Map of user IDs to sessions */ @@ -29,45 +36,38 @@ class ClientController { apiURL: import.meta.env.VITE_API_URL, }); + this.apiClient + .fetchConfiguration() + .then(() => (this.configuration = this.apiClient.configuration!)); + + this.configuration = null; this.sessions = new ObservableMap(); this.current = null; makeAutoObservable(this); + this.login = this.login.bind(this); this.logoutCurrent = this.logoutCurrent.bind(this); // Inject globally injectController("client", this); } + @action pickNextSession() { + this.current = + this.current ?? this.sessions.keys().next().value ?? null; + } + /** * Hydrate sessions and start client lifecycles. * @param auth Authentication store */ @action hydrate(auth: Auth) { for (const entry of auth.getAccounts()) { - const user_id = entry.session.user_id!; - - const session = new Session(); - this.sessions.set(user_id, session); - - session - .emit({ - action: "LOGIN", - session: entry.session, - apiUrl: entry.apiUrl, - }) - .catch((error) => { - if (error === "Forbidden" || error === "Unauthorized") { - this.sessions.delete(user_id); - auth.removeSession(user_id); - modalController.push({ type: "signed_out" }); - session.destroy(); - } - }); + this.addSession(entry); } - this.current = this.sessions.keys().next().value ?? null; + this.pickNextSession(); } @computed getActiveSession() { @@ -82,16 +82,134 @@ class ClientController { return this.getActiveSession()?.client ?? this.apiClient; } + @computed getServerConfig() { + return this.configuration; + } + @computed isLoggedIn() { return this.current === null; } + @action addSession(entry: { session: SessionPrivate; apiUrl?: string }) { + const user_id = entry.session.user_id!; + + const session = new Session(); + this.sessions.set(user_id, session); + + session + .emit({ + action: "LOGIN", + session: entry.session, + apiUrl: entry.apiUrl, + configuration: this.configuration!, + }) + .catch((error) => { + if (error === "Forbidden" || error === "Unauthorized") { + this.sessions.delete(user_id); + state.auth.removeSession(user_id); + modalController.push({ type: "signed_out" }); + session.destroy(); + } + }); + + this.pickNextSession(); + } + + async login(credentials: API.DataLogin) { + const browser = detect(); + + // Generate a friendly name for this browser + let friendly_name; + if (browser) { + let { name } = browser; + const { os } = browser; + let isiPad; + if (window.isNative) { + friendly_name = `Revolt Desktop on ${os}`; + } else { + if (name === "ios") { + name = "safari"; + } else if (name === "fxios") { + name = "firefox"; + } else if (name === "crios") { + name = "chrome"; + } + if (os === "Mac OS" && navigator.maxTouchPoints > 0) + isiPad = true; + friendly_name = `${name} on ${isiPad ? "iPadOS" : os}`; + } + } else { + friendly_name = "Unknown Device"; + } + + // Try to login with given credentials + let session = await this.apiClient.api.post("/auth/session/login", { + ...credentials, + friendly_name, + }); + + // Prompt for MFA verificaiton if necessary + if (session.result === "MFA") { + const { allowed_methods } = session; + const mfa_response: API.MFAResponse | undefined = await new Promise( + (callback) => + modalController.push({ + type: "mfa_flow", + state: "unknown", + available_methods: allowed_methods, + callback, + }), + ); + + if (typeof mfa_response === "undefined") { + throw "Cancelled"; + } + + session = await this.apiClient.api.post("/auth/session/login", { + mfa_response, + mfa_ticket: session.ticket, + friendly_name, + }); + + if (session.result === "MFA") { + // unreachable code + return; + } + } + + this.addSession({ + session, + }); + + /*const s = session; + + client.session = session; + (client as any).$updateHeaders(); + + async function login() { + state.auth.setSession(s); + } + + const { onboarding } = await client.api.get("/onboard/hello"); + + if (onboarding) { + openScreen({ + id: "onboarding", + callback: async (username: string) => + client.completeOnboarding({ username }, false).then(login), + }); + } else { + login(); + }*/ + } + @action logout(user_id: string) { const session = this.sessions.get(user_id); if (session) { this.sessions.delete(user_id); if (user_id === this.current) { - this.current = this.sessions.keys().next().value ?? null; + this.current = null; + this.pickNextSession(); } session.destroy(); diff --git a/src/controllers/client/Session.tsx b/src/controllers/client/Session.tsx index 96c203b9..e2e66b10 100644 --- a/src/controllers/client/Session.tsx +++ b/src/controllers/client/Session.tsx @@ -1,13 +1,14 @@ import { action, computed, makeAutoObservable } from "mobx"; -import { Client } from "revolt.js"; +import { API, Client } from "revolt.js"; type State = "Ready" | "Connecting" | "Online" | "Disconnected" | "Offline"; type Transition = | { action: "LOGIN"; - session: SessionPrivate; apiUrl?: string; + session: SessionPrivate; + configuration?: API.RevoltConfig; } | { action: @@ -113,6 +114,10 @@ export default class Session { this.state = "Connecting"; this.createClient(data.apiUrl); + if (data.configuration) { + this.client!.configuration = data.configuration; + } + try { await this.client!.useExistingSession(data.session); this.user_id = this.client!.user!._id; diff --git a/src/mobx/State.ts b/src/mobx/State.ts index 7a951ab9..ea1f1212 100644 --- a/src/mobx/State.ts +++ b/src/mobx/State.ts @@ -36,7 +36,7 @@ export default class State { locale: LocaleOptions; experiments: Experiments; layout: Layout; - config: ServerConfig; + private config: ServerConfig; notifications: NotificationOptions; queue: MessageQueue; settings: Settings; @@ -288,7 +288,7 @@ export default class State { } } -let state: State; +export let state: State; export async function hydrateState() { state = new State(); diff --git a/src/pages/login/forms/CaptchaBlock.tsx b/src/pages/login/forms/CaptchaBlock.tsx index 9dc8888c..c91d75e1 100644 --- a/src/pages/login/forms/CaptchaBlock.tsx +++ b/src/pages/login/forms/CaptchaBlock.tsx @@ -7,7 +7,7 @@ import { useEffect } from "preact/hooks"; import { Preloader } from "@revoltchat/ui"; -import { useApplicationState } from "../../../mobx/State"; +import { clientController } from "../../../controllers/client/ClientController"; export interface CaptchaProps { onSuccess: (token?: string) => void; @@ -15,7 +15,7 @@ export interface CaptchaProps { } export const CaptchaBlock = observer((props: CaptchaProps) => { - const configuration = useApplicationState().config.get(); + const configuration = clientController.getServerConfig(); useEffect(() => { if (!configuration?.features.captcha.enabled) { diff --git a/src/pages/login/forms/Form.tsx b/src/pages/login/forms/Form.tsx index 2ec8d0eb..2a1b7d6b 100644 --- a/src/pages/login/forms/Form.tsx +++ b/src/pages/login/forms/Form.tsx @@ -6,16 +6,14 @@ import styles from "../Login.module.scss"; import { Text } from "preact-i18n"; import { useState } from "preact/hooks"; -import { Button, Category, Preloader } from "@revoltchat/ui"; -import { Tip } from "@revoltchat/ui"; - -import { useApplicationState } from "../../../mobx/State"; +import { Button, Category, Preloader, Tip } from "@revoltchat/ui"; import { I18nError } from "../../../context/Locale"; import { takeError } from "../../../context/revoltjs/util"; import WaveSVG from "../../settings/assets/wave.svg"; +import { clientController } from "../../../controllers/client/ClientController"; import FormField from "../FormField"; import { CaptchaBlock, CaptchaProps } from "./CaptchaBlock"; import { MailProvider } from "./MailProvider"; @@ -45,7 +43,7 @@ interface FormInputs { } export const Form = observer(({ page, callback }: Props) => { - const configuration = useApplicationState().config.get(); + const configuration = clientController.getServerConfig(); const [loading, setLoading] = useState(false); const [success, setSuccess] = useState(undefined); @@ -260,7 +258,8 @@ export const Form = observer(({ page, callback }: Props) => { + target="_blank" + rel="noreferrer"> diff --git a/src/pages/login/forms/FormCreate.tsx b/src/pages/login/forms/FormCreate.tsx index 8d2e9ed7..65b4305f 100644 --- a/src/pages/login/forms/FormCreate.tsx +++ b/src/pages/login/forms/FormCreate.tsx @@ -1,9 +1,7 @@ -import { useApplicationState } from "../../../mobx/State"; - +import { useClient } from "../../../controllers/client/ClientController"; import { Form } from "./Form"; export function FormCreate() { - const config = useApplicationState().config; - const client = config.createClient(); + const client = useClient(); return
client.register(data)} />; } diff --git a/src/pages/login/forms/FormLogin.tsx b/src/pages/login/forms/FormLogin.tsx index 95b2971a..d32db521 100644 --- a/src/pages/login/forms/FormLogin.tsx +++ b/src/pages/login/forms/FormLogin.tsx @@ -1,106 +1,6 @@ -import { detect } from "detect-browser"; -import { API } from "revolt.js"; - -import { useApplicationState } from "../../../mobx/State"; - -import { useIntermediate } from "../../../context/intermediate/Intermediate"; - -import { modalController } from "../../../controllers/modals/ModalController"; +import { clientController } from "../../../controllers/client/ClientController"; import { Form } from "./Form"; export function FormLogin() { - const state = useApplicationState(); - const { openScreen } = useIntermediate(); - - return ( - { - const browser = detect(); - let friendly_name; - if (browser) { - let { name } = browser; - const { os } = browser; - let isiPad; - if (window.isNative) { - friendly_name = `Revolt Desktop on ${os}`; - } else { - if (name === "ios") { - name = "safari"; - } else if (name === "fxios") { - name = "firefox"; - } else if (name === "crios") { - name = "chrome"; - } - if (os === "Mac OS" && navigator.maxTouchPoints > 0) - isiPad = true; - friendly_name = `${name} on ${isiPad ? "iPadOS" : os}`; - } - } else { - friendly_name = "Unknown Device"; - } - - // ! FIXME: temporary login flow code - // This should be replaced in the future. - const client = state.config.createClient(); - await client.fetchConfiguration(); - - let session = await client.api.post("/auth/session/login", { - ...data, - friendly_name, - }); - - if (session.result === "MFA") { - const { allowed_methods } = session; - const mfa_response: API.MFAResponse | undefined = - await new Promise((callback) => - modalController.push({ - type: "mfa_flow", - state: "unknown", - available_methods: allowed_methods, - callback, - }), - ); - - if (typeof mfa_response === "undefined") { - throw "Cancelled"; - } - - session = await client.api.post("/auth/session/login", { - mfa_response, - mfa_ticket: session.ticket, - friendly_name, - }); - - if (session.result === "MFA") { - // unreachable code - return; - } - } - - const s = session; - - client.session = session; - (client as any).$updateHeaders(); - - async function login() { - state.auth.setSession(s); - } - - const { onboarding } = await client.api.get("/onboard/hello"); - - if (onboarding) { - openScreen({ - id: "onboarding", - callback: async (username: string) => - client - .completeOnboarding({ username }, false) - .then(login), - }); - } else { - login(); - } - }} - /> - ); + return ; }