Improve architecture

This commit is contained in:
Midgard 2020-03-26 16:01:09 +01:00
parent 190663a253
commit ae525c8904
Signed by: midgard
GPG key ID: 511C112F1331BBB4
3 changed files with 193 additions and 79 deletions

View file

@ -12,9 +12,20 @@
<tr><th>Username</th><td><input type="text" id="username"/></td></tr> <tr><th>Username</th><td><input type="text" id="username"/></td></tr>
<tr><th>Password</th><td><input type="password" id="password"/></td></tr> <tr><th>Password</th><td><input type="password" id="password"/></td></tr>
</table> </table>
<input type="button" onclick="this.disabled = true; this.value = 'Logging in...'; logIn(); return false" value="Log in"/><br/>
<input type="button" onclick="this.disabled = true; this.value = 'Validating token'; validateToken(); return false" id="validate" value="Validate token"/> </input>(ignores username and password) <input type="button" onclick="logIn(); return false" id="login" value="Log in"/>
<span id="login_message"></span><br/>
<input type="button" onclick="validateToken(); return false" id="validate" value="Validate token"/>
<span id="validate_message">(ignores username and password)</span><br/>
<input type="button" onclick="logOut(); return false" id="logout" value="Log out"/>
<span id="logout_message"></span><br/>
</form> </form>
<pre id="user_json"></pre>
<script type="text/javascript" src="xhr.js"></script> <script type="text/javascript" src="xhr.js"></script>
<script type="text/javascript" src="main.js"></script> <script type="text/javascript" src="main.js"></script>
</body> </body>

198
main.js
View file

@ -1,7 +1,5 @@
"use strict"; "use strict";
const LOCALSTORAGE_KEY_SERVERS = "mattermostServers";
function byId(id, nullOk=false) { function byId(id, nullOk=false) {
const el = document.getElementById(id); const el = document.getElementById(id);
if (!el && !nullOk) { if (!el && !nullOk) {
@ -10,57 +8,102 @@ function byId(id, nullOk=false) {
return el; 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 { class MattermostApi {
constructor(endpoint) { constructor(endpoint) {
this.endpoint = endpoint; this._endpoint = endpoint;
} }
async validateToken(token) { get id() {
const response = await ajax.getJson(`${this.endpoint}/users/me`, { return this._endpoint;
headers: { }
"Authorization": `Bearer ${token}`
} async get(path, token) {
}); const headers = token ? {"Authorization": `Bearer ${token}`} : {};
const response = await ajax.getJson(`${this._endpoint}${path}`, {headers});
if (!response.ok) { if (!response.ok) {
throw response; throw response;
} }
return response; return response;
} }
logIn(login_id, password) { async post(path, data, token) {
return ajax.postJson(`${this.endpoint}/users/login`, {login_id, password}) const headers = token ? {"Authorization": `Bearer ${token}`} : {};
.then(response => { const response = await ajax.postJson(`${this._endpoint}${path}`, data, {headers});
let token = response.getHeader("Token"); if (!response.ok) {
storeCredentials(this.endpoint, login_id, token); throw response;
return response; }
}) 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}`;
});
} }
} }
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 * Return an endpoint URL that has a protocol, domain and path
*/ */
@ -75,30 +118,81 @@ function normalizedEndpoint(endpoint) {
return `${protocol}${domain}${path}`; return `${protocol}${domain}${path}`;
} }
function logIn() { function createClient(endpoint) {
let endpoint = normalizedEndpoint(byId("server").value); const api = new MattermostApi(normalizedEndpoint(endpoint));
const storage = new Storage();
return new MattermostClient(api, storage);
}
let api = new MattermostApi(endpoint); function buttonDisable(element, text) {
api.logIn(byId("username").value, byId("password").value); 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() { 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) { if (!cred || !cred.token) {
byId("validate").value = "No token, log in first"; buttonEnable(byId("validate"), "No token, log in first");
byId("validate").disabled = false; return;
} }
let api = new MattermostApi(endpoint); client.usersMe()
api.validateToken(cred.token) .then(json => {
.then(() => { buttonEnable(byId("validate"), "Validation succeeded");
byId("validate").value = "Validation succeeded"; byId("validate_message").innerText = "";
byId("validate").disabled = false; byId("user_json").innerText = JSON.stringify(json, null, 2);
}) })
.catch(() => { .catch(error => {
byId("validate").value = "Validation failed"; console.error(error);
byId("validate").disabled = false; buttonEnable(byId("validate"), "Validation failed");
byId("validate_message").innerText = `${error}`;
}); });
} }

59
xhr.js
View file

@ -1,8 +1,15 @@
const ajax = (function() { "use strict"; const ajax = (function() { "use strict";
class NetworkError extends Error {} class AjaxError extends Error {
class UnexpectedMimeError extends Error {} constructor (message, response, ...rest) {
class InvalidJsonError extends Error {} 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"; const MIME_JSON = "application/json";
@ -33,34 +40,32 @@ function xhrInitForPromise(resolve, reject, url, method, headers) {
} }
function xhrParseJsonResponse(xhr) { function xhrParseJsonResponse(xhr) {
if (xhr.status === 0) { xhr.responseJson = null;
console.error(xhr); xhr.ok = false;
throw new NetworkError("Failed to connect to server");
}
let json; if (xhr.responseText) {
if (!xhr.responseText) {
json = null;
} else {
const contentType = xhr.getResponseHeader("Content-Type"); const contentType = xhr.getResponseHeader("Content-Type");
if (contentType != MIME_JSON) { 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 { try {
json = JSON.parse(xhr.responseText); xhr.responseJson = JSON.parse(xhr.responseText);
} catch(e) { } catch(e) {
throw new InvalidJsonError(); throw new InvalidJsonError("Server replied with JSON that we couldn't parse", xhr);
} }
} }
return { xhr.ok = 200 <= xhr.status && xhr.status < 300;
ok: 200 <= xhr.status && xhr.status < 300, if (!xhr.ok) {
status: xhr.status, console.error(xhr);
statusText: xhr.statusText, if (xhr.status === 0) {
getHeader: header => xhr.getResponseHeader(header), throw new NetworkError("Failed to connect to server. Developer console may have more information", xhr);
json, } else {
xhr, throw new NotOkError(xhr.statusText, xhr);
}; }
}
return xhr;
} }
function getJson(url, options={}) { function getJson(url, options={}) {
@ -73,7 +78,7 @@ function getJson(url, options={}) {
}).then(xhrParseJsonResponse); }).then(xhrParseJsonResponse);
} }
function postJson(url, data={}, options={}) { function postJson(url, data=undefined, options={}) {
if (!options.headers) options.headers = {}; if (!options.headers) options.headers = {};
// This triggers CORS, which is not acceptable // This triggers CORS, which is not acceptable
//options.headers["Content-Type"] = MIME_JSON; //options.headers["Content-Type"] = MIME_JSON;
@ -81,12 +86,16 @@ function postJson(url, data={}, options={}) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let xhr = xhrInitForPromise(resolve, reject, url, "POST", options.headers); 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); }).then(xhrParseJsonResponse);
} }
return { return {
NetworkError, UnexpectedMimeError, InvalidJsonError, NetworkError, NotOkError, UnexpectedMimeError, InvalidJsonError,
getJson, postJson getJson, postJson
}; };