From ae525c8904ef476ae9326b1a45b5eadf7e72d01f Mon Sep 17 00:00:00 2001 From: Midgard Date: Thu, 26 Mar 2020 16:01:09 +0100 Subject: [PATCH] Improve architecture --- index.html | 15 +++- main.js | 198 +++++++++++++++++++++++++++++++++++++++-------------- xhr.js | 59 +++++++++------- 3 files changed, 193 insertions(+), 79 deletions(-) diff --git a/index.html b/index.html index 1a615e8..4d0e5a6 100644 --- a/index.html +++ b/index.html @@ -12,9 +12,20 @@ Username Password -
- (ignores username and password) + + +
+ + + (ignores username and password)
+ + +
+ + +

+
 		
 		
 	
diff --git a/main.js b/main.js
index d2bbe09..c1030de 100644
--- a/main.js
+++ b/main.js
@@ -1,7 +1,5 @@
 "use strict";
 
-const LOCALSTORAGE_KEY_SERVERS = "mattermostServers";
-
 function byId(id, nullOk=false) {
 	const el = document.getElementById(id);
 	if (!el && !nullOk) {
@@ -10,57 +8,102 @@ function byId(id, nullOk=false) {
 	return el;
 }
 
-function storeCredentials(endpoint, login_id, token) {
-	let storedServers = JSON.parse(window.localStorage.getItem(LOCALSTORAGE_KEY_SERVERS) || "[]");
-	if (!(endpoint in storedServers)) storedServers.push(endpoint);
-	window.localStorage.setItem(LOCALSTORAGE_KEY_SERVERS, JSON.stringify(storedServers));
 
-	window.localStorage.setItem(`${LOCALSTORAGE_KEY_SERVERS}_${endpoint}`, JSON.stringify({login_id, token}));
+const LOCALSTORAGE_KEY_SERVER = "mattermostServer";
+const RE_SERVER_ITEM = new RegExp(`^${LOCALSTORAGE_KEY_SERVER}_(.*)\$`, "").compile();
+class Storage {
+	getServers() {
+		let servers = [];
+		for (var i = 0; i < window.localStorage.length; i++) {
+			const key = window.localStorage.key(i);
+			const matches = key.match(RE_SERVER_ITEM);
+			if (matches) {
+				servers.push(matches[1]);
+			}
+		}
+		return servers;
+	}
+
+	_key_for(endpoint) {
+		return `${LOCALSTORAGE_KEY_SERVER}_${endpoint}`;
+	}
+
+	clear(endpoint) {
+		window.localStorage.removeItem(this._key_for(endpoint));
+	}
+
+	store(endpoint, login_id, token) {
+		window.localStorage.setItem(this._key_for(endpoint), JSON.stringify({login_id, token}));
+	}
+
+	get(endpoint) {
+		return JSON.parse(window.localStorage.getItem(this._key_for(endpoint)) || "null");
+	}
 }
 
-function getCredentials(endpoint) {
-	return JSON.parse(window.localStorage.getItem(`${LOCALSTORAGE_KEY_SERVERS}_${endpoint}`) || "null");
-}
 
 class MattermostApi {
 	constructor(endpoint) {
-		this.endpoint = endpoint;
+		this._endpoint = endpoint;
 	}
 
-	async validateToken(token) {
-		const response = await ajax.getJson(`${this.endpoint}/users/me`, {
-			headers: {
-				"Authorization": `Bearer ${token}`
-			}
-		});
+	get id() {
+		return this._endpoint;
+	}
+
+	async get(path, token) {
+		const headers = token ? {"Authorization": `Bearer ${token}`} : {};
+		const response = await ajax.getJson(`${this._endpoint}${path}`, {headers});
 		if (!response.ok) {
 			throw response;
 		}
 		return response;
 	}
 
-	logIn(login_id, password) {
-		return ajax.postJson(`${this.endpoint}/users/login`, {login_id, password})
-			.then(response => {
-				let token = response.getHeader("Token");
-				storeCredentials(this.endpoint, login_id, token);
-				return response;
-			})
-			.then(response => {
-				document.body.innerHTML = "";
-				const pre = document.createElement("pre");
-				pre.innerText = JSON.stringify(response.json, null, 2);
-				document.body.appendChild(pre);
-
-				return response;
-			})
-			.catch(error => {
-				console.error(error);
-				document.body.innerText = `An error occurred: ${error}`;
-			});
+	async post(path, data, token) {
+		const headers = token ? {"Authorization": `Bearer ${token}`} : {};
+		const response = await ajax.postJson(`${this._endpoint}${path}`, data, {headers});
+		if (!response.ok) {
+			throw response;
+		}
+		return response;
 	}
 }
 
+
+class MattermostClient {
+	constructor (api, storage) {
+		this.api = api;
+		this.storage = storage;
+	}
+
+	async logIn(login_id, password) {
+		const response = await this.api.post("/users/login", {login_id, password});
+		const token = response.getResponseHeader("Token");
+		if (!token) {
+			throw Error("No Token header in response to log in request");
+		}
+		this.storage.store(this.api.id, login_id, token);
+		return response.responseJson;
+	}
+
+	async logOut() {
+		const response = await this.api.post("/users/logout");
+		//this.storage.clear(this.api.id);
+		return response.responseJson;
+	}
+
+	async usersMe() {
+		const stored = this.storage.get(this.api.id);
+		if (!stored || !stored.token) {
+			throw Error("No token stored");
+		}
+		const response = await this.api.get("/users/me", stored.token);
+		return response.responseJson;
+	}
+}
+
+
 /**
  * Return an endpoint URL that has a protocol, domain and path
  */
@@ -75,30 +118,81 @@ function normalizedEndpoint(endpoint) {
 	return `${protocol}${domain}${path}`;
 }
 
-function logIn() {
-	let endpoint = normalizedEndpoint(byId("server").value);
+function createClient(endpoint) {
+	const api = new MattermostApi(normalizedEndpoint(endpoint));
+	const storage = new Storage();
+	return new MattermostClient(api, storage);
+}
 
-	let api = new MattermostApi(endpoint);
-	api.logIn(byId("username").value, byId("password").value);
+function buttonDisable(element, text) {
+	element.value = text;
+	element.disabled = true;
+}
+function buttonEnable(element, text) {
+	element.value = text;
+	element.disabled = false;
+}
+
+function logIn() {
+	byId("user_json").innerText = "";
+
+	const client = createClient(byId("server").value);
+
+	buttonDisable(byId("login"), "Logging in...");
+
+	client.logIn(byId("username").value, byId("password").value)
+		.then(json => {
+			buttonEnable(byId("login"), "Logged in");
+			byId("login_message").innerText = "";
+			byId("user_json").innerText = JSON.stringify(json, null, 2);
+		})
+		.catch(error => {
+			console.error(error);
+			buttonEnable(byId("login"), "Could not log in");
+			byId("login_message").innerText = `${error}`;
+		});
+}
+
+function logOut() {
+	const client = createClient(byId("server").value);
+
+	buttonDisable(byId("logout"), "Logging out...");
+
+	client.logOut()
+		.then(response => {
+			buttonEnable(byId("logout"), "Logged out");
+			byId("logout_message").innerText = "";
+			byId("user_json").innerText = "";
+		})
+		.catch(error => {
+			console.error(error);
+			buttonEnable(byId("logout"), "Could not log out");
+			byId("logout_message").innerText = `${error}`;
+		});
 }
 
 function validateToken() {
-	let endpoint = normalizedEndpoint(byId("server").value);
+	byId("user_json").innerText = "";
 
-	let cred = getCredentials(endpoint);
+	const client = createClient(byId("server").value);
+
+	buttonDisable(byId("validate"), "Validating token...");
+
+	let cred = client.storage.get(client.api.id);
 	if (!cred || !cred.token) {
-		byId("validate").value = "No token, log in first";
-		byId("validate").disabled = false;
+		buttonEnable(byId("validate"), "No token, log in first");
+		return;
 	}
 
-	let api = new MattermostApi(endpoint);
-	api.validateToken(cred.token)
-		.then(() => {
-			byId("validate").value = "Validation succeeded";
-			byId("validate").disabled = false;
+	client.usersMe()
+		.then(json => {
+			buttonEnable(byId("validate"), "Validation succeeded");
+			byId("validate_message").innerText = "";
+			byId("user_json").innerText = JSON.stringify(json, null, 2);
 		})
-		.catch(() => {
-			byId("validate").value = "Validation failed";
-			byId("validate").disabled = false;
+		.catch(error => {
+			console.error(error);
+			buttonEnable(byId("validate"), "Validation failed");
+			byId("validate_message").innerText = `${error}`;
 		});
 }
diff --git a/xhr.js b/xhr.js
index 02c866d..51ff20a 100644
--- a/xhr.js
+++ b/xhr.js
@@ -1,8 +1,15 @@
 const ajax = (function() { "use strict";
 
-class NetworkError extends Error {}
-class UnexpectedMimeError extends Error {}
-class InvalidJsonError extends Error {}
+class AjaxError extends Error {
+	constructor (message, response, ...rest) {
+		super(message, ...rest);
+		this.response = response;
+	}
+}
+class NetworkError extends AjaxError {}
+class NotOkError extends AjaxError {}
+class UnexpectedMimeError extends AjaxError {}
+class InvalidJsonError extends AjaxError {}
 
 const MIME_JSON = "application/json";
 
@@ -33,34 +40,32 @@ function xhrInitForPromise(resolve, reject, url, method, headers) {
 }
 
 function xhrParseJsonResponse(xhr) {
-	if (xhr.status === 0) {
-		console.error(xhr);
-		throw new NetworkError("Failed to connect to server");
-	}
+	xhr.responseJson = null;
+	xhr.ok = false;
 
-	let json;
-	if (!xhr.responseText) {
-		json = null;
-	} else {
+	if (xhr.responseText) {
 		const contentType = xhr.getResponseHeader("Content-Type");
 		if (contentType != MIME_JSON) {
-			throw new UnexpectedMimeError(`Server did not reply with JSON but with ${contentType}`);
+			throw new UnexpectedMimeError(`Server did not reply with JSON but with ${contentType}`, xhr);
 		}
 		try {
-			json = JSON.parse(xhr.responseText);
+			xhr.responseJson = JSON.parse(xhr.responseText);
 		} catch(e) {
-			throw new InvalidJsonError();
+			throw new InvalidJsonError("Server replied with JSON that we couldn't parse", xhr);
 		}
 	}
 
-	return {
-		ok: 200 <= xhr.status && xhr.status < 300,
-		status: xhr.status,
-		statusText: xhr.statusText,
-		getHeader: header => xhr.getResponseHeader(header),
-		json,
-		xhr,
-	};
+	xhr.ok = 200 <= xhr.status && xhr.status < 300;
+	if (!xhr.ok) {
+		console.error(xhr);
+		if (xhr.status === 0) {
+			throw new NetworkError("Failed to connect to server. Developer console may have more information", xhr);
+		} else {
+			throw new NotOkError(xhr.statusText, xhr);
+		}
+	}
+
+	return xhr;
 }
 
 function getJson(url, options={}) {
@@ -73,7 +78,7 @@ function getJson(url, options={}) {
 	}).then(xhrParseJsonResponse);
 }
 
-function postJson(url, data={}, options={}) {
+function postJson(url, data=undefined, options={}) {
 	if (!options.headers) options.headers = {};
 	// This triggers CORS, which is not acceptable
 	//options.headers["Content-Type"] = MIME_JSON;
@@ -81,12 +86,16 @@ function postJson(url, data={}, options={}) {
 
 	return new Promise((resolve, reject) => {
 		let xhr = xhrInitForPromise(resolve, reject, url, "POST", options.headers);
-		xhr.send(JSON.stringify(data));
+		if (data === undefined) {
+			xhr.send();
+		} else {
+			xhr.send(JSON.stringify(data));
+		}
 	}).then(xhrParseJsonResponse);
 }
 
 return {
-	NetworkError, UnexpectedMimeError, InvalidJsonError,
+	NetworkError, NotOkError, UnexpectedMimeError, InvalidJsonError,
 	getJson, postJson
 };