diff --git a/src/App.tsx b/src/App.tsx
index e5d7869..a6b5400 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -5,7 +5,7 @@ import * as localforage from "localforage";
import Matrix from "./Matrix";
import Login from "./Login";
import Guide from "./Guide";
-import { LoginData } from "./types";
+import { LoginData, WellKnown } from "./types";
interface AppState {
state: string | null;
@@ -15,12 +15,14 @@ interface AppState {
class App extends Component<{}, AppState> {
private loginData: null | LoginData;
+ private well_known: null | WellKnown;
private timeout: null | number;
public state: AppState;
constructor(props: {}) {
super(props);
this.loginData = null;
+ this.well_known = null;
this.timeout = null;
this.state = {
state: null,
@@ -35,6 +37,9 @@ class App extends Component<{}, AppState> {
this.setState({ state: "login" });
}
});
+ localforage.getItem("well_known").then((well_known: unknown) => {
+ this.well_known = well_known as WellKnown;
+ });
localforage.getItem("guide").then((value: unknown) => {
this.setState({ guide: Boolean(value) });
});
@@ -78,7 +83,7 @@ class App extends Component<{}, AppState> {
}
if (state === "matrix") {
- return ;
+ return ;
}
alert("Some error occured. This must not happen");
window.close();
diff --git a/src/Login.tsx b/src/Login.tsx
index 5ce6583..933b06b 100644
--- a/src/Login.tsx
+++ b/src/Login.tsx
@@ -1,11 +1,7 @@
import { Component } from "inferno";
-import { createClient } from "matrix-js-sdk";
-import * as localforage from "localforage";
import { TextListItem, TextInput, Header, SoftKey, ListViewKeyed } from "KaiUI";
-import shared from "./shared";
-import { fetch as customFetch } from "./fetch";
-import { WellKnown } from "./types";
import LoginWithQR from "./LoginWithQR";
+import LoginHandler from "./LoginHandler";
import { LoginFlow } from "matrix-js-sdk/lib/@types/auth";
interface LoginState {
@@ -15,14 +11,13 @@ interface LoginState {
}
class Login extends Component<{}, LoginState> {
- private homeserverUrl: string;
- private homeserverName: string;
- private loginFlows: LoginFlow[];
private readonly stageNames: string[];
private selectedLoginFlow?: LoginFlow;
+ public state: LoginState;
+ private loginHandler: LoginHandler;
+ private homeserverName: string;
private username: string;
private password: string;
- public state: LoginState;
cursorChangeCb = (cursor: number) => {
this.setState({ cursor: cursor });
@@ -59,79 +54,18 @@ class Login extends Component<{}, LoginState> {
}
};
- doLogin = () => {
- if (!this.selectedLoginFlow) {
- throw new Error("selectedLoginFlow is not defined");
- }
- switch (this.selectedLoginFlow.type) {
- case "m.login.password":
- shared.mClient
- .loginWithPassword(
- `@${this.username}:${this.homeserverName}`,
- this.password
- )
- .then((result: any) => {
- localforage.setItem("login", result).then(() => {
- alert("Logged in as " + this.username);
- window.location = window.location;
- });
- })
- .catch((err: any) => {
- switch (err.errcode) {
- case "M_FORBIDDEN":
- alert("Incorrect login credentials");
- break;
- case "M_USER_DEACTIVATED":
- alert("This account has been deactivated");
- break;
- case "M_LIMIT_EXCEEDED":
- const retry = Math.ceil(err.retry_after_ms / 1000);
- alert("Too many requests! Retry after" + retry.toString());
- break;
- default:
- alert("Login failed for some unknown reason");
- break;
- }
- });
- break;
- default:
- alert("Invalid/unsupported login method. This is likely a bug");
- break;
- }
- };
-
rightCb = () => {
switch (this.state.stage) {
case 0:
- this.homeserverName = this.homeserverName.replace("https://", "");
- this.homeserverName = this.homeserverName.replace("http://", "");
- fetch(`https://${this.homeserverName}/.well-known/matrix/client`)
- .then((r: Response) => {
- if (r.ok) {
- r.json().then((j: WellKnown) => {
- this.homeserverUrl = j["m.homeserver"].base_url;
- shared.mClient = createClient({
- baseUrl: this.homeserverUrl,
- fetchFn: customFetch,
- });
- shared.mClient
- .loginFlows()
- .then((result) => {
- this.loginFlows = result.flows;
- this.setState({ cursor: 0, stage: 1 });
- })
- .catch((e: any) => console.log(e));
- });
- } else {
- alert(
- "Cannot connect to homeserver. Are you sure the address valid?"
- );
- }
- })
- .catch((e) => console.log(e));
+ this.loginHandler.findHomeserver(this.homeserverName).then( () => {
+ this.setState({ cursor: 0, stage: 1})
+ }).catch((e: any) => {
+ window.alert("Could not connect to homeserver");
+ console.log(e);
+ });
break;
case 1:
- this.selectedLoginFlow = this.loginFlows[this.state.cursor];
+ this.selectedLoginFlow = this.loginHandler.loginFlows[this.state.cursor];
if (this.selectedLoginFlow.type !== "m.login.password") {
window.alert("The selected login method is not implemented, yet.");
} else {
@@ -139,7 +73,14 @@ class Login extends Component<{}, LoginState> {
}
break;
case 2:
- this.doLogin();
+ let loginData = {'username': this.username, 'password': this.password};
+ if (this.selectedLoginFlow !== undefined) {
+ this.loginHandler.doLogin(this.selectedLoginFlow, loginData).then(() => {
+ window.location = window.location;
+ }).catch((e) => alert(e.message));
+ } else {
+ throw new Error("Undefined selectedLoginFlow")
+ }
break;
default:
alert("Invalid stage!");
@@ -155,12 +96,11 @@ class Login extends Component<{}, LoginState> {
constructor(props: any) {
super(props);
- this.stageNames = ["Login Info", "Login method", "Login"];
- this.loginFlows = [];
+ this.homeserverName = "";
this.username = "";
this.password = "";
- this.homeserverName = "";
- this.homeserverUrl = "";
+ this.stageNames = ["Login Info", "Login method", "Login"];
+ this.loginHandler = new LoginHandler()
this.state = {
stage: 0,
cursor: 0,
@@ -178,7 +118,8 @@ class Login extends Component<{}, LoginState> {
render() {
if (this.state.loginWithQr) {
- return ;
+ return ;
+ // return ;
}
let listViewChildren;
switch (this.state.stage) {
@@ -204,7 +145,7 @@ class Login extends Component<{}, LoginState> {
},
{
tertiary:
- "Press Call button and scan a QR in the following format to login with QR code instead of typing all these(PASS = password authentication): PASS server_name username password",
+ "Press Call button and scan a QR in the following format to login with QR code instead of typing all these (PASS = password authentication): PASS server_name username password",
type: "text",
key: "qrHint",
},
@@ -214,7 +155,7 @@ class Login extends Component<{}, LoginState> {
});
break;
case 1:
- listViewChildren = this.loginFlows.map((flow: LoginFlow) => {
+ listViewChildren = this.loginHandler.loginFlows.map((flow: LoginFlow) => {
return ;
});
break;
diff --git a/src/LoginHandler.ts b/src/LoginHandler.ts
new file mode 100644
index 0000000..38fb6c8
--- /dev/null
+++ b/src/LoginHandler.ts
@@ -0,0 +1,118 @@
+import { fetch as customFetch } from "./fetch";
+import { WellKnown } from "./types";
+import { LoginFlow } from "matrix-js-sdk/lib/@types/auth";
+import * as localforage from "localforage";
+import { createClient } from "matrix-js-sdk";
+import shared from "./shared";
+
+export default class LoginHandler {
+ public base_url: string;
+ public homeserverName: string;
+ public username: string;
+ public password: string;
+ public loginFlows: LoginFlow[];
+
+ constructor() {
+ this.homeserverName = "";
+ this.username = "";
+ this.password = "";
+ this.loginFlows = [];
+ this.base_url = "";
+ }
+
+ private setWellKnown(well_known: WellKnown) {
+ return localforage.setItem("well_known", well_known);
+ }
+
+ public async doLogin(loginFlow: LoginFlow, loginData: any) {
+ // TODO implement more login flows
+ // Instead of implementing them one by one, consider using mClient.login
+ // and passing loginData (which needs to be properly formed according to their spec)
+ // (may or may not be a bad idea)
+ try {
+ let loginResult: any;
+ let username: string = `@${loginData.username}:${this.homeserverName}`;
+ switch (loginFlow.type) {
+ case "m.login.password":
+ let password: string = loginData.password;
+ loginResult = await shared.mClient
+ .loginWithPassword(username, password);
+ break;
+ default:
+ throw new Error("Unsupported");
+ break;
+ }
+ if (loginResult.well_known) {
+ this.setWellKnown(loginResult.well_known)
+ console.log("Received a well_known from client login property. Updating previous settings.")
+ console.log(loginResult.well_known)
+ }
+ await localforage.setItem("login", loginResult);
+ alert("Logged in as " + username);
+ } catch (e: any) {
+ let message: string;
+ switch (e.errcode) {
+ case "M_FORBIDDEN":
+ message = "Incorrect login credentials";
+ break;
+ case "M_USER_DEACTIVATED":
+ message = "This account has been deactivated";
+ break;
+ case "M_LIMIT_EXCEEDED":
+ const retry = Math.ceil(e.retry_after_ms / 1000);
+ message = `Too many requests! Retry after ${retry.toString()}`;
+ break;
+ default:
+ if (e.message === "Unsupported") {
+ message = "Login flow selected is unsupported"
+ } else if (e.errcode) {
+ message = e.errcode;
+ } else {
+ message = `Login failed for some unknown reason: ${e.message}`;
+ }
+ break;
+ }
+ throw new Error(message)
+ }
+ }
+
+ public async findHomeserver(name: string) {
+ name = name.replace("https://", "");
+ name = name.replace("http://", "");
+ this.homeserverName = name;
+ let base_url: string = "";
+ let well_known_url = `https://${name}/.well-known/matrix/client`;
+ try {
+ let r: Response = await fetch(well_known_url);
+ if (!r.ok) {
+ throw new Error("404");
+
+ }
+ let well_known: WellKnown = await r.json();
+ base_url = well_known["m.homeserver"].base_url;
+ } catch (e: any) {
+ console.log(`.well-known not found or malformed at ${well_known_url}`);
+ base_url = "https://" + name;
+ } finally {
+ this.base_url = base_url;
+ try {
+ shared.mClient = createClient({
+ baseUrl: base_url,
+ fetchFn: customFetch,
+ });
+ let result = await shared.mClient.loginFlows()
+ if (! result.flows) {
+ throw new Error("Got no flows");
+ }
+ this.loginFlows = result.flows;
+ } catch (e) {
+ alert(`No server found at ${base_url}`)
+ console.log(e);
+ }
+ this.setWellKnown({
+ "m.homeserver": {"base_url": base_url},
+ "m.identity_server": {"base_url": "https://vector.im"}, // TODO Where to infer this outside of actual .well-known?
+ })
+ }
+ }
+}
diff --git a/src/LoginWithQR/LoginWithQR.tsx b/src/LoginWithQR/LoginWithQR.tsx
index b7ba8eb..f6ea2ae 100644
--- a/src/LoginWithQR/LoginWithQR.tsx
+++ b/src/LoginWithQR/LoginWithQR.tsx
@@ -1,16 +1,18 @@
import { Component } from "inferno";
import QrScanner from "qr-scanner";
-import { createClient } from "matrix-js-sdk";
-import * as localforage from "localforage";
+import { LoginFlow } from "matrix-js-sdk/lib/@types/auth";
import "./LoginWithQR.css";
import { SoftKey } from "KaiUI";
-import shared from "../shared";
-import { fetch as customFetch } from "../fetch";
-import { WellKnown } from "../types";
+import LoginHandler from "../LoginHandler";
-class LoginWithQR extends Component<{}, {}> {
+interface LoginWithQRProps {
+ loginHandler: LoginHandler;
+}
+
+class LoginWithQR extends Component {
private video?: HTMLVideoElement;
+ private loginHandler: LoginHandler;
startScanning = () => {
if (!this.video) {
@@ -27,104 +29,52 @@ class LoginWithQR extends Component<{}, {}> {
scanner.start();
};
- doLogin = (data: string) => {
+ private login_flows_short: {[key: string]: string} = {
+ "PASS": "m.login.password"
+ }
+
+ private async doLogin (data: string) {
let decodedParts: string[] = data.split(" ", 4);
- const flow = decodedParts[0];
+ let flow = decodedParts[0];
const server_name = decodedParts[1];
const username = decodedParts[2];
- let password: string;
- if (
- window.confirm(
- `Do you confirm? Flow: ${flow} | Server name: ${server_name} | Username: ${username}`
- )
- ) {
- const start: number =
- flow.length + server_name.length + username.length + 3;
- password = data.substring(start);
- fetch(`https://${server_name}/.well-known/matrix/client`).then(
- (r: Response) => {
- if (r.ok) {
- r.json()
- .then((j: WellKnown) => {
- const server_url: string = j["m.homeserver"].base_url;
- shared.mClient = createClient({
- baseUrl: server_url,
- fetchFn: customFetch,
- });
- shared.mClient
- .loginFlows()
- .then((result) => {
- let gotPasswordLogin = false;
- for (let flow of result.flows) {
- if ("m.login.password" === flow.type) {
- gotPasswordLogin = true;
- break;
- }
- }
- if (gotPasswordLogin) {
- shared.mClient
- .loginWithPassword(
- `@${username}:${server_name}`,
- password
- )
- .then((result: any) => {
- localforage.setItem("login", result).then(() => {
- window.alert("Logged in as " + username);
- window.location = window.location;
- });
- })
- .catch((err: any) => {
- switch (err.errcode) {
- case "M_FORBIDDEN":
- alert("Incorrect login credentials");
- break;
- case "M_USER_DEACTIVATED":
- alert("This account has been deactivated");
- break;
- case "M_LIMIT_EXCEEDED":
- const retry = Math.ceil(
- err.retry_after_ms / 1000
- );
- alert(
- "Too many requests! Retry after" +
- retry.toString()
- );
- break;
- default:
- alert("Login failed! Unknown reason");
- break;
- }
- // eslint-disable-next-line no-self-assign
- window.location = window.location;
- });
- } else {
- window.alert(
- "This homeserver does not support authentication with password"
- );
- }
- })
- .catch((e) => {
- window.alert("Error getting login flows from the server");
- console.log(e);
- });
- })
- .catch((e) => {
- window.alert("Error getting information about the server");
- console.log("REPORT", e);
- });
- } else {
- alert(
- "Cannot connect to homeserver. Are you sure the address is correct?"
- );
+ const start = flow.length + server_name.length + username.length + 3;
+ let password: string = data.substring(start);
+ // TODO implement more flows
+ if (window.confirm(
+ `Do you confirm? Flow: ${flow} | Server name: ${server_name} | Username: ${username}`)) {
+ // users can either write the full m.login.password (or whatever other flow) or use a shorthand
+ // This maps the shorthand to the actual flow identificator
+ if (!flow.startsWith("m.login")) {
+ flow = this.login_flows_short[flow];
+ }
+ if (flow !== "m.login.password") {
+ alert("Password authentication is the only supported flow currently")
+ return;
+ }
+ try {
+ await this.loginHandler.findHomeserver(server_name);
+ let selected_flow: LoginFlow | undefined;
+ for (let available_flow of this.loginHandler.loginFlows) {
+ if (available_flow.type === flow) {
+ selected_flow = available_flow;
}
}
- );
+ if (selected_flow !== undefined) {
+ let loginData = {'username': username, 'password': password};
+ await this.loginHandler.doLogin(selected_flow, loginData);
+ window.location = window.location;
+ }
+ } catch (e) {
+ alert(e)
+ }
}
};
constructor(props: any) {
super(props);
this.state = null;
+ this.loginHandler = props.loginHandler;
}
componentDidMount() {
diff --git a/src/Matrix.tsx b/src/Matrix.tsx
index 8348008..8790e5f 100644
--- a/src/Matrix.tsx
+++ b/src/Matrix.tsx
@@ -27,6 +27,7 @@ const vapidPublicKey =
interface MatrixProps {
data: any;
+ well_known: any;
}
interface Call {
@@ -321,10 +322,10 @@ class Matrix extends Component {
userId: props.data.user_id,
accessToken: props.data.access_token,
deviceId: props.data.device_id,
- baseUrl: props.data.well_known["m.homeserver"].base_url,
+ baseUrl: props.well_known["m.homeserver"].base_url,
identityServer:
- props.data.well_known["m.identity_server"] &&
- props.data.well_known["m.identity_server"].base_url,
+ props.well_known["m.identity_server"] &&
+ props.well_known["m.identity_server"].base_url,
// store: new matrixcs.IndexedDBStore({ indexedDB: window.indexedDB, localStorage: window.localStorage }),
});
const client = shared.mClient;