Skip to content

Commit 3371d0f

Browse files
author
Greg Perkins
committed
export zip file containing csv, attachments, html, json
make attachment downloads safe (not open to anyone who knows the id)
1 parent 68a4fe3 commit 3371d0f

File tree

5 files changed

+360
-25
lines changed

5 files changed

+360
-25
lines changed

client/dashboard/dashboard.vue

Lines changed: 75 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
}
3838
.clickable { cursor: pointer; }
3939
.nowrap { white-space: nowrap; }
40+
select.rightify { text-align-last: right; }
4041
4142
div.filter {
4243
position: sticky;
@@ -57,7 +58,7 @@
5758
.thelayout {
5859
padding-top: 80px;
5960
display: grid;
60-
grid-template-columns: 150px 1fr 350px;
61+
grid-template-columns: 150px 1fr 425px;
6162
grid-template-areas: "gleft gmiddle gright";
6263
}
6364
.theleft {
@@ -108,7 +109,7 @@
108109
</div>
109110
<div v-for="m in messages" :key="m.messageId" class="ui raised fluid card">
110111
<div class="content">
111-
<div class="right floated time">
112+
<div class="right floated time nowrap">
112113
<a data-tooltip='filter UNTIL this time' @click="addTimeFilter(m, 'Until')"><i class="chevron left icon"></i></a>
113114
<small>{{m.receivedText}}</small>
114115
<a class="icobut" data-tooltip='filter SINCE this time' @click="addTimeFilter(m, 'Since')"><i class="chevron right icon"></i></a>
@@ -145,7 +146,7 @@
145146
</div>
146147
</div>
147148
<div v-if="m.attachmentIds.length" class="content">
148-
<a :href="`/api/vault/attachment/${a}/v1`" v-for="(a,i) of m.attachmentIds" class="butspacer ui compact mini button">
149+
<a @click="getAttachment(m, i)" v-for="(_, i) of m.attachmentIds" data-tooltip="click to download" class="butspacer ui compact mini button">
149150
<i class="download icon"></i> {{attachmentName(m, i)}}
150151
</a>
151152
</div>
@@ -171,10 +172,10 @@
171172
<div class="filter-section ui form">
172173
<form class="ui form">
173174
<div class="fields">
174-
<select v-model="pageSize" class="ui selection dropdown" @change="offset=0">
175-
<option v-for="limit in selectablePageSizes" :value="limit">{{limit + ' Messages per Page'}}</option>
175+
<select v-model="pageSize" class="ui selection dropdown rightify" @change="offset=0">
176+
<option v-for="limit in selectablePageSizes" :value="limit">{{limit + ' Results / Page&nbsp;'}}</option>
176177
</select>
177-
<select v-model="ascending" class="ui fluid selection dropdown">
178+
<select v-model="ascending" class="ui selection dropdown">
178179
<option value="yes">Oldest First</option>
179180
<option value="no">Newest First</option>
180181
</select>
@@ -187,7 +188,7 @@
187188
<div class="fields" style="margin-bottom:0;">
188189
<input class="ui input" type="text" v-model="enteredText" placeholder="Add and Update Text Filters">
189190
</div>
190-
<small><em><span class="nowrap">body words</span> | <span class="nowrap"><b>title:</b>words</span> | <span class="nowrap"><b>to:</b>fragment</span> | <span class="nowrap"><b>from:</b>fragment</span> | <span class="nowrap"><b>has:</b>[no] attach[ment[s]]</span></em></small>
191+
<small><em><span class="nowrap">body words</span> | <span class="nowrap"><b>title:</b> words</span> | <span class="nowrap"><b>to:</b> fragment</span> | <span class="nowrap"><b>from: </b>fragment</span> | <span class="nowrap"><b>has: </b>[no] attach[ment[s]]</span></em></small>
191192
</form>
192193
</div>
193194
<div class="filter-section" v-if="Object.keys(filters).length">
@@ -199,7 +200,7 @@
199200
<div class="filter-section">
200201
</div>
201202
<div v-if="fullCount" class="export">
202-
<button class="ui fluid button" @click="exportData">Export {{fullCount}} Result{{fullCount == 1 ? '' : 's'}}</button>
203+
<button class="ui fluid primary button" :class="{loading: exporting}" @click="getExport">Export {{fullCount}} Result{{fullCount == 1 ? '' : 's'}}</button>
203204
</div>
204205
</div>
205206
</div>
@@ -208,13 +209,64 @@
208209

209210
<script>
210211
211-
moment = require('moment');
212+
const moment = require('moment');
213+
const util = require('../util');
212214
213215
const REFRESH_POLL_RATE = 15000;
214216
215217
const PAGE_SIZES = [5, 10, 20, 50, 100, 1000];
216218
const DEFAULT_PAGE_SIZE = PAGE_SIZES[1];
217219
220+
async function getExport(queryString, acceptType) {
221+
let result;
222+
try {
223+
result = await util.fetch.call(this, '/api/vault/export/v1?' + queryString, { headers: { 'Accept': acceptType } });
224+
} catch (err) {
225+
console.error('had error', err);
226+
return;
227+
}
228+
229+
if (result.ok) {
230+
const blob = await result.blob();
231+
const anchor = document.createElement('a');
232+
const burl = window.URL.createObjectURL(blob);
233+
anchor.href = burl;
234+
anchor.style = "display: none";
235+
anchor.download = result.headers.get('content-disposition').match(/ filename="(.*?)"/)[1];
236+
document.body.appendChild(anchor);
237+
anchor.click();
238+
setTimeout(() => {
239+
document.body.removeChild(anchor);
240+
window.URL.revokeObjectURL(burl);
241+
}, 500);
242+
}
243+
}
244+
245+
async function getAttachment(id, acceptType) {
246+
let result;
247+
try {
248+
result = await util.fetch.call(this, `/api/vault/attachment/${id}/v1`, { headers: { 'Accept': acceptType } });
249+
} catch (err) {
250+
console.error('had error', err);
251+
return;
252+
}
253+
254+
if (result.ok) {
255+
const blob = await result.blob();
256+
const anchor = document.createElement('a');
257+
const burl = window.URL.createObjectURL(blob);
258+
anchor.href = burl;
259+
anchor.style = "display: none";
260+
anchor.download = result.headers.get('content-disposition').match(/ filename="(.*?)"/)[1];
261+
document.body.appendChild(anchor);
262+
anchor.click();
263+
setTimeout(() => {
264+
document.body.removeChild(anchor);
265+
window.URL.revokeObjectURL(burl);
266+
}, 500);
267+
}
268+
}
269+
218270
function extract(text, regex, action) {
219271
let stripped = text;
220272
let match;
@@ -239,6 +291,7 @@ module.exports = {
239291
fullCount: 0,
240292
offset: 0,
241293
ascending: 'no',
294+
exporting: false,
242295
messages: []
243296
}),
244297
computed: {
@@ -247,6 +300,7 @@ module.exports = {
247300
q.push(`offset=${this.offset}`);
248301
q.push(`limit=${this.pageSize}`);
249302
q.push(`ascending=${this.ascending}`);
303+
q.push(`tzoffset=${(new Date()).getTimezoneOffset()}`);
250304
return q.join('&').replace("'","");
251305
},
252306
pagerDots: function() {
@@ -349,6 +403,17 @@ module.exports = {
349403
this.fullCount = (this.messages.length && this.messages[0].fullCount) || 0;
350404
});
351405
},
406+
getExport: function() {
407+
this.exporting = true;
408+
const q = this.queryString;
409+
getExport(q, 'application/zip').then(() => { this.exporting = false; });
410+
},
411+
getAttachment: function(m, idx) {
412+
const message = m.payload.find(x => x.version === 1);
413+
const attachment = message && message.data && message.data.attachments[idx];
414+
const id = m.attachmentIds[idx];
415+
getAttachment(m.attachmentIds[idx], attachment.type);
416+
},
352417
messageBody: function(m) {
353418
const message = m.payload.find(x => x.version === 1);
354419
const tmpText = message.data && message.data.body.find(x => x.type === 'text/plain');
@@ -368,15 +433,11 @@ module.exports = {
368433
return attachment && attachment.name;
369434
},
370435
threadColor: function(id) {
371-
// map id to a darkish color from a clumpy, visually-differentiable collection
436+
// map id to a randomish darkish color from a clumpy, visually-differentiable collection
372437
const val = parseInt(id.replace('-', ''), 16);
373438
const hue = (val % 90) * 4;
374439
const lum = (val % 5) * 10 + 20;
375-
376440
return { color: `hsl(${hue}, 100%, ${lum}%)` };
377-
},
378-
exportData: function() {
379-
alert('TBD');
380441
}
381442
},
382443
mounted: function() {

client/util.js

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,9 @@ async function _fetch(url, { method='get', headers={}, body={} }={}, noBodyAwait
3939
const resp = await fetch(url, parms);
4040
if (noBodyAwaits) return resp;
4141

42-
const text = await resp.text();
4342
if ((resp.headers.get('content-type') || '').startsWith('application/json')) {
43+
const text = await resp.text();
4444
resp.theJson = JSON.parse(text.trim() || '{}');
45-
} else {
46-
resp.theText = text;
4745
}
4846
if (resp.status === 401) {
4947
console.log('401 from bot api, so we will visit bot authentication...');

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
"message-vault": "./server/index.js"
2525
},
2626
"dependencies": {
27+
"adm-zip": "^0.4.7",
2728
"bcrypt": "^1.0.3",
2829
"browserify": "^14.5.0",
2930
"csv-stringify": "2.0.0",

0 commit comments

Comments
 (0)