mirror of
https://github.com/revoltchat/revite.git
synced 2024-11-22 07:00:58 -05:00
feat: make login functional again
This commit is contained in:
parent
8d505c9564
commit
66ae518e51
7 changed files with 160 additions and 140 deletions
|
@ -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();
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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)} />;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue