feat: make login functional again

This commit is contained in:
Paul Makles 2022-06-29 10:52:42 +01:00
parent 8d505c9564
commit 66ae518e51
7 changed files with 160 additions and 140 deletions

View file

@ -1,8 +1,10 @@
import { detect } from "detect-browser";
import { action, computed, makeAutoObservable, ObservableMap } from "mobx"; 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 { injectController } from "../../lib/window";
import { state } from "../../mobx/State";
import Auth from "../../mobx/stores/Auth"; import Auth from "../../mobx/stores/Auth";
import { modalController } from "../modals/ModalController"; import { modalController } from "../modals/ModalController";
@ -14,6 +16,11 @@ class ClientController {
*/ */
private apiClient: Client; private apiClient: Client;
/**
* Server configuration
*/
private configuration: API.RevoltConfig | null;
/** /**
* Map of user IDs to sessions * Map of user IDs to sessions
*/ */
@ -29,45 +36,38 @@ class ClientController {
apiURL: import.meta.env.VITE_API_URL, apiURL: import.meta.env.VITE_API_URL,
}); });
this.apiClient
.fetchConfiguration()
.then(() => (this.configuration = this.apiClient.configuration!));
this.configuration = null;
this.sessions = new ObservableMap(); this.sessions = new ObservableMap();
this.current = null; this.current = null;
makeAutoObservable(this); makeAutoObservable(this);
this.login = this.login.bind(this);
this.logoutCurrent = this.logoutCurrent.bind(this); this.logoutCurrent = this.logoutCurrent.bind(this);
// Inject globally // Inject globally
injectController("client", this); injectController("client", this);
} }
@action pickNextSession() {
this.current =
this.current ?? this.sessions.keys().next().value ?? null;
}
/** /**
* Hydrate sessions and start client lifecycles. * Hydrate sessions and start client lifecycles.
* @param auth Authentication store * @param auth Authentication store
*/ */
@action hydrate(auth: Auth) { @action hydrate(auth: Auth) {
for (const entry of auth.getAccounts()) { for (const entry of auth.getAccounts()) {
const user_id = entry.session.user_id!; this.addSession(entry);
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.current = this.sessions.keys().next().value ?? null; this.pickNextSession();
} }
@computed getActiveSession() { @computed getActiveSession() {
@ -82,16 +82,134 @@ class ClientController {
return this.getActiveSession()?.client ?? this.apiClient; return this.getActiveSession()?.client ?? this.apiClient;
} }
@computed getServerConfig() {
return this.configuration;
}
@computed isLoggedIn() { @computed isLoggedIn() {
return this.current === null; 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) { @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); this.sessions.delete(user_id);
if (user_id === this.current) { if (user_id === this.current) {
this.current = this.sessions.keys().next().value ?? null; this.current = null;
this.pickNextSession();
} }
session.destroy(); session.destroy();

View file

@ -1,13 +1,14 @@
import { action, computed, makeAutoObservable } from "mobx"; 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 State = "Ready" | "Connecting" | "Online" | "Disconnected" | "Offline";
type Transition = type Transition =
| { | {
action: "LOGIN"; action: "LOGIN";
session: SessionPrivate;
apiUrl?: string; apiUrl?: string;
session: SessionPrivate;
configuration?: API.RevoltConfig;
} }
| { | {
action: action:
@ -113,6 +114,10 @@ export default class Session {
this.state = "Connecting"; this.state = "Connecting";
this.createClient(data.apiUrl); this.createClient(data.apiUrl);
if (data.configuration) {
this.client!.configuration = data.configuration;
}
try { try {
await this.client!.useExistingSession(data.session); await this.client!.useExistingSession(data.session);
this.user_id = this.client!.user!._id; this.user_id = this.client!.user!._id;

View file

@ -36,7 +36,7 @@ export default class State {
locale: LocaleOptions; locale: LocaleOptions;
experiments: Experiments; experiments: Experiments;
layout: Layout; layout: Layout;
config: ServerConfig; private config: ServerConfig;
notifications: NotificationOptions; notifications: NotificationOptions;
queue: MessageQueue; queue: MessageQueue;
settings: Settings; settings: Settings;
@ -288,7 +288,7 @@ export default class State {
} }
} }
let state: State; export let state: State;
export async function hydrateState() { export async function hydrateState() {
state = new State(); state = new State();

View file

@ -7,7 +7,7 @@ import { useEffect } from "preact/hooks";
import { Preloader } from "@revoltchat/ui"; import { Preloader } from "@revoltchat/ui";
import { useApplicationState } from "../../../mobx/State"; import { clientController } from "../../../controllers/client/ClientController";
export interface CaptchaProps { export interface CaptchaProps {
onSuccess: (token?: string) => void; onSuccess: (token?: string) => void;
@ -15,7 +15,7 @@ export interface CaptchaProps {
} }
export const CaptchaBlock = observer((props: CaptchaProps) => { export const CaptchaBlock = observer((props: CaptchaProps) => {
const configuration = useApplicationState().config.get(); const configuration = clientController.getServerConfig();
useEffect(() => { useEffect(() => {
if (!configuration?.features.captcha.enabled) { if (!configuration?.features.captcha.enabled) {

View file

@ -6,16 +6,14 @@ import styles from "../Login.module.scss";
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";
import { useState } from "preact/hooks"; import { useState } from "preact/hooks";
import { Button, Category, Preloader } from "@revoltchat/ui"; import { Button, Category, Preloader, Tip } from "@revoltchat/ui";
import { Tip } from "@revoltchat/ui";
import { useApplicationState } from "../../../mobx/State";
import { I18nError } from "../../../context/Locale"; import { I18nError } from "../../../context/Locale";
import { takeError } from "../../../context/revoltjs/util"; import { takeError } from "../../../context/revoltjs/util";
import WaveSVG from "../../settings/assets/wave.svg"; import WaveSVG from "../../settings/assets/wave.svg";
import { clientController } from "../../../controllers/client/ClientController";
import FormField from "../FormField"; import FormField from "../FormField";
import { CaptchaBlock, CaptchaProps } from "./CaptchaBlock"; import { CaptchaBlock, CaptchaProps } from "./CaptchaBlock";
import { MailProvider } from "./MailProvider"; import { MailProvider } from "./MailProvider";
@ -45,7 +43,7 @@ interface FormInputs {
} }
export const Form = observer(({ page, callback }: Props) => { export const Form = observer(({ page, callback }: Props) => {
const configuration = useApplicationState().config.get(); const configuration = clientController.getServerConfig();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [success, setSuccess] = useState<string | undefined>(undefined); const [success, setSuccess] = useState<string | undefined>(undefined);
@ -260,7 +258,8 @@ export const Form = observer(({ page, callback }: Props) => {
<a <a
href="https://developers.revolt.chat/faq/instances#what-is-a-third-party-instance" href="https://developers.revolt.chat/faq/instances#what-is-a-third-party-instance"
style={{ color: "var(--accent)" }} style={{ color: "var(--accent)" }}
target="_blank" rel="noreferrer"> target="_blank"
rel="noreferrer">
<Text id="general.learn_more" /> <Text id="general.learn_more" />
</a> </a>
</span> </span>

View file

@ -1,9 +1,7 @@
import { useApplicationState } from "../../../mobx/State"; import { useClient } from "../../../controllers/client/ClientController";
import { Form } from "./Form"; import { Form } from "./Form";
export function FormCreate() { export function FormCreate() {
const config = useApplicationState().config; const client = useClient();
const client = config.createClient();
return <Form page="create" callback={(data) => client.register(data)} />; return <Form page="create" callback={(data) => client.register(data)} />;
} }

View file

@ -1,106 +1,6 @@
import { detect } from "detect-browser"; import { clientController } from "../../../controllers/client/ClientController";
import { API } from "revolt.js";
import { useApplicationState } from "../../../mobx/State";
import { useIntermediate } from "../../../context/intermediate/Intermediate";
import { modalController } from "../../../controllers/modals/ModalController";
import { Form } from "./Form"; import { Form } from "./Form";
export function FormLogin() { export function FormLogin() {
const state = useApplicationState(); return <Form page="login" callback={clientController.login} />;
const { openScreen } = useIntermediate();
return (
<Form
page="login"
callback={async (data) => {
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();
}
}}
/>
);
} }