Skip to content

Commit 4b6b70c

Browse files
author
Greg Perkins
committed
add settings page and management of administrators
1 parent 18ab2eb commit 4b6b70c

File tree

7 files changed

+289
-50
lines changed

7 files changed

+289
-50
lines changed

client/dashboard/dashboard.vue

Lines changed: 5 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@
7979
user-select: none;
8080
}
8181
div.pager .active-page {
82-
color: #FF8888;
82+
color: #FF6666;
8383
}
8484
8585
div.zero {
@@ -193,7 +193,7 @@
193193
</div>
194194
<div class="filter-section" v-if="Object.keys(filters).length">
195195
<h3>Current Filters</h3>
196-
<a v-for="(v,k) in filters" @click="removeFilter(k)" data-tooltip="click to remove filter" class="butspacer ui compact blue button">
196+
<a v-for="(v,k) in filters" @click="removeFilter(k)" data-tooltip="click to remove filter" class="butspacer ui compact primary basic button">
197197
<i class="remove icon"></i> {{v.presentation}}
198198
</a>
199199
</div>
@@ -392,14 +392,14 @@ module.exports = {
392392
const q = this.queryString;
393393
util.fetch.call(this, '/api/vault/messages/v1?' + q)
394394
.then(result => {
395-
this.messages = result.theJson.messages.forEach(m => {
395+
this.messages = result.theJson.messages;
396+
this.messages.forEach(m => {
396397
m.receivedMoment = moment(m.received);
397398
m.receivedText = m.receivedMoment.format('llll');
398399
if (m.recipientIds.length <= 5 && !(m.messageId in this.showDist)) {
399400
this.$set(this.showDist, m.messageId, true);
400401
}
401402
});
402-
this.messages = result.theJson.messages;
403403
this.fullCount = (this.messages.length && this.messages[0].fullCount) || 0;
404404
});
405405
},
@@ -441,22 +441,7 @@ module.exports = {
441441
}
442442
},
443443
mounted: function() {
444-
if (this.global.onboardStatus !== 'complete') {
445-
this.$router.push({ name: 'welcome' });
446-
return;
447-
}
448-
util.fetch.call(this, '/api/onboard/status/v1')
449-
.then(result => {
450-
this.global.onboardStatus = result.theJson.status;
451-
if (this.global.onboardStatus !== 'complete') {
452-
this.$router.push({ name: 'welcome' });
453-
}
454-
});
455-
456-
if (!this.global.apiToken) {
457-
this.$router.push({ name: 'loginTag', query: { forwardTo: this.$router.path }});
458-
return;
459-
}
444+
util.checkPrerequisites.call(this);
460445
461446
this.getMessages();
462447
this.interval = setInterval(() => this.getMessages(), REFRESH_POLL_RATE);

client/main.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ function main() {
1212
{ path: '/onboard/tag', name: 'onboardTag', component: require('./onboard/enterTag.vue') },
1313
{ path: '/onboard/code/:tag', name: 'onboardCode', component: require('./onboard/enterCode.vue') },
1414
{ path: '/dashboard', name: 'dashboard', component: require('./dashboard/dashboard.vue') },
15+
{ path: '/settings', name: 'settings', component: require('./settings/settings.vue') },
1516
{ path: '*', redirect: '/welcome' },
1617
];
1718

client/menu/top.vue

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,43 @@
1-
<style>
2-
</style>
3-
41
<template>
52
<div class="ui fixed inverted menu" style="z-index: 1;">
63
<div class="ui container">
74
<router-link :to="{name: 'welcome'}" class="header item">
85
<img class="logo" src="/static/images/forsta-logo-invert.svg"/>
96
&nbsp;&nbsp;Forsta Message Vault
107
</router-link>
11-
<a v-if="global.apiToken" class="header item float right" @click.prevent="logout">
12-
sign out
13-
</a>
8+
<div v-if="global.apiToken" class="header item float right" style="padding:0;">
9+
<div class="ui simple dropdown item">
10+
<i class="large user icon"></i>
11+
<i class="dropdown icon"></i>
12+
<div class="menu left">
13+
<div class="item" @click="logout">sign out</div>
14+
<div class="item" @click="settings">settings</div>
15+
<div class="item" @click="dashboard">dashboard</div>
16+
</div>
17+
</div>
18+
</div>
1419
</div>
1520
</div>
1621
</template>
1722

1823
<script>
1924
shared = require('../globalState');
20-
util = require('../util');
2125
2226
module.exports = {
2327
data: () => ({
2428
global: shared.state,
2529
loggedIn: false
2630
}),
27-
computed: {
28-
},
2931
methods: {
3032
logout: function () {
31-
if (this.global.apiToken) {
32-
console.log('signing out');
33-
this.global.apiToken = null;
34-
}
33+
this.global.apiToken = null;
34+
},
35+
settings: function () {
36+
this.$router.push({ name: 'settings' });
37+
},
38+
dashboard: function () {
39+
this.$router.push({ name: 'dashboard' });
3540
}
36-
},
37-
watch: {
38-
},
39-
mounted: function() {
40-
},
41+
}
4142
}
4243
</script>

client/settings/settings.vue

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
2+
<style>
3+
div.listgap {
4+
margin-bottom:3em!important;
5+
}
6+
</style>
7+
8+
<template>
9+
<div class="ui main text container" style="margin-top: 80px;">
10+
<div class="ui container center aligned">
11+
<div class="ui basic segment huge">
12+
<h1 class="ui header">
13+
<i class="large circular setting icon"></i>
14+
Message Vault Settings
15+
</h1>
16+
</div>
17+
<div class="ui centered grid">
18+
<div class="ui nine wide column basic segment left aligned b1" :class="{loading: loading}" style="margin-top:-1em;">
19+
<h3 style="margin-bottom: 3px;">Site Administrators</h3>
20+
<div class="ui list listgap">
21+
<div v-for="a in admins" :key="a.id" class="item">
22+
<a v-if="admins.length > 1" @click="removeAdmin(a.id)" data-tooltip="remove this administrator"><i class="large remove circle icon"></i></a>
23+
<span v-else data-tooltip="cannot remove last administrator"><i style="color: lightgray;" class="large remove circle icon"></i></span>
24+
{{a.label}}
25+
</div>
26+
</div>
27+
<form class="ui large form enter-tag" @submit.prevent="addAdmin">
28+
<div class="field" :class="{error:!!tagError}">
29+
<div data-tooltip="add an administrator" class="ui left icon action input">
30+
<i class="at icon"></i>
31+
<input type="text" v-model='tag' name="tag" placeholder="user:org" autocomplete="off">
32+
<button class="ui icon button" :disabled="!tag" :class="{primary:!!tag}"><i class="plus icon"></i></button>
33+
</div>
34+
</div>
35+
</form>
36+
<div v-if="tagError" class="ui small error message">{{tagError}}</div>
37+
</div>
38+
</div>
39+
</div>
40+
</div>
41+
</template>
42+
43+
<script>
44+
45+
const util = require('../util');
46+
const REFRESH_POLL_RATE = 15000;
47+
48+
49+
async function addAdmin() {
50+
this.loading = true;
51+
let result;
52+
try {
53+
result = await util.fetch.call(this, '/api/auth/admins/v1', { method: 'post', body: { op: 'add', tag: this.tag }})
54+
this.loading = false;
55+
} catch (err) {
56+
console.error(err);
57+
this.loading = false;
58+
return false;
59+
}
60+
if (result.ok) {
61+
const { administrators } = result.theJson;
62+
this.admins = administrators;
63+
this.tag = '';
64+
this.tagError = '';
65+
} else {
66+
this.tagError = util.mergeErrors(result.theJson);
67+
}
68+
}
69+
70+
async function removeAdmin(id) {
71+
this.loading = true;
72+
let result;
73+
try {
74+
result = await util.fetch.call(this, '/api/auth/admins/v1', { method: 'post', body: { op: 'remove', id }})
75+
this.loading = false;
76+
} catch (err) {
77+
console.error(err);
78+
this.loading = false;
79+
return false;
80+
}
81+
if (result.ok) {
82+
const { administrators } = result.theJson;
83+
this.admins = administrators;
84+
} else {
85+
this.removeError = util.mergeErrors(result.theJson);
86+
}
87+
}
88+
89+
90+
module.exports = {
91+
data: () => ({
92+
global: shared.state,
93+
loading: false,
94+
interval: null,
95+
tag: '',
96+
tagError: '',
97+
removeError: '',
98+
admins: []
99+
}),
100+
computed: {
101+
},
102+
watch: {
103+
},
104+
methods: {
105+
getAdmins: function() {
106+
util.fetch.call(this, '/api/auth/admins/v1')
107+
.then(result => {
108+
if (result.ok) {
109+
this.admins = result.theJson.administrators;
110+
}
111+
});
112+
},
113+
removeAdmin: function(id) {
114+
removeAdmin.call(this, id);
115+
},
116+
addAdmin: function() {
117+
addAdmin.call(this);
118+
}
119+
},
120+
mounted: function() {
121+
util.checkPrerequisites.call(this);
122+
this.getAdmins();
123+
this.interval = setInterval(() => this.getAdmins(), REFRESH_POLL_RATE);
124+
},
125+
beforeDestroy: function() {
126+
clearInterval(this.interval);
127+
}
128+
}
129+
</script>

client/util.js

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,25 @@ async function _fetch(url, { method='get', headers={}, body={} }={}, noBodyAwait
5151
return resp;
5252
}
5353

54+
function checkPrerequisites() {
55+
_fetch.call(this, '/api/onboard/status/v1')
56+
.then(result => {
57+
this.global.onboardStatus = result.theJson.status;
58+
if (this.global.onboardStatus !== 'complete') {
59+
this.$router.push({ name: 'welcome' });
60+
}
61+
});
62+
63+
if (!this.global.apiToken) {
64+
this.$router.push({ name: 'loginTag', query: { forwardTo: this.$router.path }});
65+
return;
66+
}
67+
}
68+
5469
module.exports = {
5570
addFormErrors,
5671
mergeErrors,
5772
RequestError,
58-
fetch: _fetch
73+
fetch: _fetch,
74+
checkPrerequisites
5975
};

server/forsta_bot.js

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ class ForstaBot {
8686
}
8787
fqLabel(user) { return `${this.fqTag(user)} (${this.fqName(user)})`; }
8888

89+
8990
async onMessage(ev) {
9091
const received = new Date(ev.data.timestamp);
9192
const envelope = JSON.parse(ev.data.message.body);
@@ -197,7 +198,7 @@ class ForstaBot {
197198
const auth = this.genAuthCode(1);
198199
this.msgSender.send({
199200
distribution: resolved,
200-
threadId: await this.getSoloAuthThreadId(),
201+
threadId: await this.getGroupAuthThreadId(),
201202
text: `${auth.code} is your authentication code, valid for one minute`
202203
});
203204
const pending = await relay.storage.get('authentication', 'pending', {});
@@ -225,8 +226,79 @@ class ForstaBot {
225226
delete pending[userId];
226227
relay.storage.set('authentication', 'pending', pending);
227228

229+
await this.broadcastNotice('has successfully authenticated as an administrator', userId);
228230
return true;
229231
}
232+
233+
async getAdministrators() {
234+
const adminIds = await relay.storage.get('authentication', 'adminIds', []);
235+
const adminUsers = await this.getUsers(adminIds);
236+
const admins = adminUsers.map(u => {
237+
return {
238+
id: u.id,
239+
label: this.fqLabel(u)
240+
};
241+
});
242+
return admins;
243+
}
244+
245+
async broadcastNotice(action, actorUserId) {
246+
const adminIds = await relay.storage.get('authentication', 'adminIds', []);
247+
let added = false;
248+
if (!adminIds.includes(actorUserId)) {
249+
adminIds.push(actorUserId);
250+
added = true;
251+
}
252+
const adminUsers = await this.getUsers(adminIds);
253+
const actor = adminUsers.find(u => u.id === actorUserId);
254+
const actorLabel = actor ? this.fqLabel(actor) : '<unknown>';
255+
const expression = adminUsers.map(u => this.fqTag(u)).join(' + ');
256+
const distribution = await this.resolveTags(expression);
257+
258+
const adminList = adminUsers.filter(u => !(added && u.id === actorUserId)).map(u => this.fqLabel(u)).join('\n');
259+
260+
const fullMessage = `Note: ${actorLabel} ${action}.\n\nCurrent administrators are:\n${adminList}`;
261+
const subbedFullMessage = fullMessage.replace(/<<([^>]*)>>/g, (_, id) => {
262+
const user = adminUsers.find(x => x.id === id);
263+
return this.fqLabel(user);
264+
});
265+
266+
this.msgSender.send({
267+
distribution,
268+
threadId: await this.getSoloAuthThreadId(),
269+
text: subbedFullMessage
270+
});
271+
}
272+
273+
async addAdministrator({addTag, actorUserId}) {
274+
const tag = (addTag && addTag[0] === '@') ? addTag : '@' + addTag;
275+
const resolved = await this.resolveTags(tag);
276+
if (resolved.userids.length === 1 && resolved.warnings.length === 0) {
277+
const uid = resolved.userids[0];
278+
const adminIds = await relay.storage.get('authentication', 'adminIds');
279+
if (!adminIds.includes(uid)) {
280+
adminIds.push(uid);
281+
await relay.storage.set('authentication', 'adminIds', adminIds);
282+
}
283+
await this.broadcastNotice(`has added <<${uid}>> to the administrator list`, actorUserId);
284+
return this.getAdministrators();
285+
}
286+
throw { statusCode: 400, info: { tag: ['not a recognized tag, please try again'] } };
287+
}
288+
289+
async removeAdministrator({removeId, actorUserId}) {
290+
const adminIds = await relay.storage.get('authentication', 'adminIds', []);
291+
const idx = adminIds.indexOf(removeId);
292+
293+
if (idx < 0) {
294+
throw { statusCode: 400, info: { id: ['administrator id not found'] } };
295+
}
296+
adminIds.splice(idx, 1);
297+
await this.broadcastNotice(`is removing <<${removeId}>> from the administrator list`, actorUserId);
298+
await relay.storage.set('authentication', 'adminIds', adminIds);
299+
300+
return this.getAdministrators();
301+
}
230302
}
231303

232304
module.exports = ForstaBot;

0 commit comments

Comments
 (0)