Make Client remember its token, add ID form checks

Refactor MattermostClient, deduplicating code and making it remember its token.

Make functions that ask for a Mattermost ID check that they get
something of the correct form.
This commit is contained in:
Midgard 2020-03-31 18:12:03 +02:00
parent 88876414bc
commit 596cd63fb5
Signed by: midgard
GPG key ID: 511C112F1331BBB4
6 changed files with 80 additions and 48 deletions

View file

@ -64,7 +64,7 @@
<script type="text/javascript" src="/js/ajax.js"></script> <script type="text/javascript" src="/js/ajax.js"></script>
<script type="text/javascript" src="/js/util.js"></script> <script type="text/javascript" src="/js/util.js"></script>
<script type="text/javascript" src="/js/model/credentials.js"></script> <script type="text/javascript" src="/js/model/localstorage_credentials.js"></script>
<script type="text/javascript" src="/js/model/mm_client.js"></script> <script type="text/javascript" src="/js/model/mm_client.js"></script>
<script type="text/javascript" src="/js/view/view.js"></script> <script type="text/javascript" src="/js/view/view.js"></script>
<script type="text/javascript" src="/js/controller/controller.js"></script> <script type="text/javascript" src="/js/controller/controller.js"></script>

View file

@ -2,7 +2,7 @@
function createClient(endpoint) { function createClient(endpoint) {
const api = new mm_client.MattermostApi(normalizedEndpoint(endpoint)); const api = new mm_client.MattermostApi(normalizedEndpoint(endpoint));
return new mm_client.MattermostClient(api); return new mm_client.MattermostClient(api, localstorage_credentials);
} }
function buttonDisable(element, text) { function buttonDisable(element, text) {

View file

@ -1,4 +1,4 @@
const credentials = (function() { "use strict"; const localstorage_credentials = (function() { "use strict";
const LOCALSTORAGE_KEY_SERVER = "mattermostServer"; const LOCALSTORAGE_KEY_SERVER = "mattermostServer";
const RE_SERVER_ITEM = new RegExp(`^${LOCALSTORAGE_KEY_SERVER}_(.*)\$`, ""); const RE_SERVER_ITEM = new RegExp(`^${LOCALSTORAGE_KEY_SERVER}_(.*)\$`, "");

View file

@ -31,74 +31,84 @@ class MattermostApi {
class MattermostClient { class MattermostClient {
constructor (api) { constructor (api, credentials_provider) {
this.api = api; this.api = api;
this.credentials = credentials_provider;
const creds = this.credentials.get(this.api.id);
this.token = creds ? creds.token : null;
console.info(`Created MattermostClient for ${this.api.id}, ${this.token ? "found token" : "did not find token"}`);
}
async authenticatedGet(url, queryParams) {
assert(this.token, "logged in");
const response = await this.api.get(url, this.token, queryParams);
return response.responseJson;
}
async loggedIn() {
if (!this.token) {
return false;
}
try {
const meResponse = await this.userMe();
return true;
} catch (e) {
if (e instanceof ajax.NotOkError && e.xhr.status == 401) {
return false;
} else {
throw e;
}
}
} }
async logIn(login_id, password) { async logIn(login_id, password) {
if (this.token && await this.tokenWorks()) {
throw Error("Already logged in on this server");
}
const response = await this.api.post("/users/login", undefined, {login_id, password}); const response = await this.api.post("/users/login", undefined, {login_id, password});
const token = response.getResponseHeader("Token"); const token = response.getResponseHeader("Token");
if (!token) { if (!token) {
throw Error("No Token header in response to log in request"); throw Error("No Token header in response to log in request");
} }
credentials.store(this.api.id, login_id, token); this.credentials.store(this.api.id, login_id, token);
this.token = token;
return response.responseJson; return response.responseJson;
} }
async logOut() { async logOut() {
const stored = credentials.get(this.api.id); assert(this.token, "logged in");
if (!stored || !stored.token) { const response = await this.api.post("/users/logout", this.token);
throw Error("No token stored");
}
const response = await this.api.post("/users/logout", stored.token);
// Verify that the token is now invalidated // Verify that the token is now invalidated
let tokenWorks; if (await loggedIn()) {
try {
const meResponse = await this.usersMe();
tokenWorks = true;
} catch (e) {
if (e instanceof ajax.NotOkError && e.xhr.status == 401) {
tokenWorks = false;
} else {
throw e;
}
}
if (tokenWorks) {
throw new Error("Failed to log out: token still works after trying to log out"); throw new Error("Failed to log out: token still works after trying to log out");
} }
credentials.clear(this.api.id); this.credentials.clear(this.api.id);
this.token = null;
return response.responseJson; return response.responseJson;
} }
async usersMe() { user(user_id) {
const stored = credentials.get(this.api.id); assertIsMattermostId(user_id);
if (!stored || !stored.token) { return this.authenticatedGet(`/users/${user_id}`);
throw Error("No token stored"); }
} userMe() { return this.authenticatedGet("/users/me"); }
const response = await this.api.get("/users/me", stored.token); myTeams() { return this.authenticatedGet("/users/me/teams"); }
return response.responseJson;
myChannels(team_id) {
assertIsMattermostId(team_id);
return this.authenticatedGet(`/users/me/teams/${team_id}/channels`);
} }
async myTeams() { channelPosts(channel_id, beforePost=null, afterPost=null, since=null) {
const stored = credentials.get(this.api.id); assertIsMattermostId(channel_id);
const response = await this.api.get("/users/me/teams", stored.token); assertIsNullOrMattermostId(beforePost);
return response.responseJson; assertIsNullOrMattermostId(afterPost);
} return this.authenticatedGet(`/channels/${channel_id}/posts`, {
async myChannels(team_id) {
const stored = credentials.get(this.api.id);
const response = await this.api.get(`/users/me/teams/${team_id}/channels`, stored.token);
return response.responseJson;
}
async channelPosts(channel_id, beforePost=null, afterPost=null, since=null) {
const stored = credentials.get(this.api.id);
const response = await this.api.get(`/channels/${channel_id}/posts`, stored.token, {
before: beforePost, after: afterPost, since before: beforePost, after: afterPost, since
}); });
return response.responseJson;
} }
} }

View file

@ -44,3 +44,25 @@ function thisToArg(f) {
} }
} }
class AssertionError extends Error {}
/**
* Throw an AssertionError if the first argument is not true
*/
function assert(condition, message) {
if (!condition) {
if (message) {
throw new AssertionError(`Assertion failed: ${message}`);
} else {
throw new AssertionError("Assertion failed");
}
}
}
const MATTERMOST_ID_REGEXP = /^[a-z0-9]{26}$/;
function assertIsMattermostId(string, name="") {
assert(MATTERMOST_ID_REGEXP.test(string), `${name} has the form of a Mattermost ID`);
}
function assertIsNullOrMattermostId(string, name="") {
assert(string === null || MATTERMOST_ID_REGEXP.test(string), `${name} is null or has the form of a Mattermost ID`);
}

View file

@ -1,5 +1,5 @@
function populateServerSelectionList() { function populateServerSelectionList() {
const servers = credentials.getServers(); const servers = localstorage_credentials.getServers();
let nodes = []; let nodes = [];
for (let server of servers) { for (let server of servers) {
@ -49,7 +49,7 @@ function populateChannelList() {
} }
} }
const servers = credentials.getServers(); const servers = localstorage_credentials.getServers();
byId("channel_list").innerHTML = ""; byId("channel_list").innerHTML = "";
for (let server of servers) { for (let server of servers) {