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>Password</th><td><input type="password" id="password"/></td></tr>
</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>
<pre id="user_json"></pre>
<script type="text/javascript" src="xhr.js"></script>
<script type="text/javascript" src="main.js"></script>
</body>

198
main.js
View File

@ -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}`;
});
}

59
xhr.js
View File

@ -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
};