Added file upload capabilties via the add pin dialog or drag-and-drop on the board page

This commit is contained in:
slynn1324 2021-10-04 14:27:29 -05:00
parent 8ee0acda17
commit 0f3fc05594
11 changed files with 690 additions and 54 deletions

View file

@ -71,6 +71,8 @@ app.addSetter('load.board', async (data, force) => {
}); });
app.addSetter('load.user', async (data) => { app.addSetter('load.user', async (data) => {
console.log("load.user");
store.do("loader.show"); store.do("loader.show");
let res = await fetch("/api/whoami"); let res = await fetch("/api/whoami");
@ -96,6 +98,102 @@ app.addSetter("hash.update", (data) => {
} }
}); });
app.addSetter("app.uploadDroppedFiles", async (data, evt) => {
let boardId = store.data.board.id;
if ( boardId ){
let hasFiles = event.dataTransfer.types.find(i => i == "Files") == "Files";
if ( hasFiles ){
if ( evt.dataTransfer.items ){
let files = [];
for ( let i = 0; i < evt.dataTransfer.items.length; ++i ){
if ( evt.dataTransfer.items[i].kind === "file" ){
let file = evt.dataTransfer.items[i].getAsFile();
if ( file.type != "image/jpeg" && file.type != "image/png" ){
window.alert("Unsupported file type. JPEG and PNG images are supported.");
console.log("Unsupported file type: " + file.type);
return;
}
// check size
if ( file.size >= 26214400 ){
window.alert("File size exceeds the 25MB limit.");
console.log("File size exceeds the 25MB limit. size=" + file.size);
document.getElementById("fileInput").value = "";
return;
}
files.push(file);
}
}
console.log("Number of files=" + files.length);
for ( let i = 0; i < files.length; ++i ){
data.dropUploadMessage = `Uploading ${i+1} of ${files.length}`;
try {
let newPin = await multipartUpload(files[i], boardId);
if ( data.board && data.board.id == boardId ){
data.board.pins.push(newPin);
}
} catch (e){
window.alert("Error uploading images.");
break;
}
}
data.dropUploadMessage = null;
}
}
}
});
function PostException(statusCode, errorMessage){
this.statusCode = statusCode;
this.errorMessage = errorMessage;
}
async function multipartUpload(file, boardId, newBoardName, siteUrl, description){
console.log("attempting multipart upload");
let formData = new FormData();
formData.append("file", file);
formData.append("boardId", boardId);
if ( newBoardName ){
formData.append("newBoardName", newBoardName);
}
if ( siteUrl ){
formData.append("siteUrl", siteUrl);
}
if ( description ){
formData.append("description", description);
}
let res = await fetch("./multiup", {
method: "POST",
body: formData
});
if ( res.status == 200 ){
return res.json();
} else {
console.error("error uploading status=" + res.status + " body=" + await res.text());
throw new PostException(res.status);
}
}
function dispatchSocketConnect(){ function dispatchSocketConnect(){
window.dispatchEvent(new CustomEvent("socket-connect")); window.dispatchEvent(new CustomEvent("socket-connect"));
} }
@ -121,7 +219,8 @@ let store = new Reef.Store({
previewImageUrl: null, previewImageUrl: null,
siteUrl: "", siteUrl: "",
description: "", description: "",
saveInProgress: false saveInProgress: false,
didYouKnowDragAndDropMessageDisabled: window.localStorage.addPinModal_didYouKnowDragAndDropMessageDisabled == "true" || false
}, },
pinZoomModal: { pinZoomModal: {
active: false, active: false,
@ -167,6 +266,23 @@ const appComponent = new Reef("#app", {
<div id="editBoardModal"></div> <div id="editBoardModal"></div>
<div id="aboutModal"></div> <div id="aboutModal"></div>
<div id="editPinModal"></div> <div id="editPinModal"></div>
<div id="dragAndDropModal" class="modal">
<div class="modal-background"></div>
<div class="modal-content">
<div class="box">
<div class="m-6">drop to add pins</div>
</div>
</div>
</div>
<div class="modal ${data.dropUploadMessage ? 'is-active' : ''}">
<div class="modal-background"></div>
<div class="modal-content has-text-centered">
<div class="box">
<div class="button is-text is-large is-loading"></div>
<div>${data.dropUploadMessage}</div>
</div>
</div>
</div>
` `
//<div id="loader" class="button is-text ${data.loading ? 'is-loading' : ''}"></div> //<div id="loader" class="button is-text ${data.loading ? 'is-loading' : ''}"></div>
} }
@ -290,4 +406,50 @@ document.addEventListener("visibilitychange", async () => {
} }
} }
}); });
window.dragInProgress = false;
window.ondragover = (evt) => {
let data = store.data;
if ( !data.board || data.addPinModal.active || data.editPinModal.active || data.aboutModal.active || data.pinZoomModal.active ){
return;
}
evt.preventDefault();
let hasFiles = event.dataTransfer.types.find(i => i == "Files") == "Files";
if ( hasFiles ){
window.dragInProgress = true;
document.getElementById("dragAndDropModal").classList.add("is-active");
}
};
window.ondragleave = (evt) => {
if ( evt.x == 0 && evt.y == 0 ){
document.getElementById("dragAndDropModal").classList.remove("is-active");
window.dragInProgress = false;
}
}
window.ondrop = async (evt) => {
if ( window.dragInProgress ){
evt.preventDefault();
document.getElementById("dragAndDropModal").classList.remove("is-active");
let hasFiles = event.dataTransfer.types.find(i => i == "Files") == "Files";
if ( hasFiles ){
store.do("app.uploadDroppedFiles", evt);
}
}
};

View file

@ -437,4 +437,15 @@ body.socketConnected #socketConnected {
.is-touch .navbar.is-light .navbar-brand .navbar-link:hover, .is-touch .navbar.is-light .navbar-brand .navbar-link:hover,
.is-touch .navbar.is-light .navbar-brand .navbar-link.is-active { .is-touch .navbar.is-light .navbar-brand .navbar-link.is-active {
background-color: transparent; background-color: transparent;
}
#dragAndDropModal .modal-background{
background: #fff;
}
#dragAndDropModal .box {
background: none;
border: 3px dashed #000;
text-align: center;
} }

View file

@ -21,9 +21,18 @@ app.addSetter('addPinModal.close', (data) => {
data.addPinModal.description = ""; data.addPinModal.description = "";
data.addPinModal.newBoardName = ""; data.addPinModal.newBoardName = "";
data.addPinModal.saveInProgress = false; data.addPinModal.saveInProgress = false;
data.addPinModal.uploadFile = null;
// weird hack to pick up whether it redraws or not...
let fileInput = document.getElementById("fileInput");
if ( fileInput ){
fileInput.value = "";
}
}); });
app.addSetter('addPinModal.updatePreview', (data) => { app.addSetter('addPinModal.updatePreview', (data) => {
if ( data.addPinModal.imageUrl.startsWith("http") ){ if ( data.addPinModal.imageUrl.startsWith("http") ){
( async() => { ( async() => {
let res = await fetch(data.addPinModal.imageUrl, { let res = await fetch(data.addPinModal.imageUrl, {
@ -38,6 +47,7 @@ app.addSetter('addPinModal.updatePreview', (data) => {
} else { } else {
data.addPinModal.previewImageUrl = null; data.addPinModal.previewImageUrl = null;
} }
}); });
app.addSetter('addPinModal.save', async (data) => { app.addSetter('addPinModal.save', async (data) => {
@ -48,37 +58,63 @@ app.addSetter('addPinModal.save', async (data) => {
let boardId = data.addPinModal.boardId; let boardId = data.addPinModal.boardId;
let postData = {
boardId: boardId,
newBoardName: data.addPinModal.newBoardName,
imageUrl: data.addPinModal.imageUrl,
siteUrl: data.addPinModal.siteUrl,
description: data.addPinModal.description
};
let res = await fetch('api/pins', { if ( data.addPinModal.uploadFile ){
method: 'POST', // do file upload
headers: {
'Content-Type': "application/json" console.log("attempting multipart file uploading");
},
body: JSON.stringify(postData) try {
}); let newPin = await multipartUpload(data.addPinModal.uploadFile, boardId, data.addPinModal.newBoardName, data.addPinModal.siteUrl, data.addPinModal.description);
if ( data.board && data.board.id == boardId ){
if ( res.status == 200 ){ data.board.pins.push(newPin);
}
let body = await res.json();
if ( data.board && data.board.id == boardId ){ window.localStorage.addPinLastBoardId = boardId;
data.board.pins.push(body); store.do("addPinModal.close");
if ( boardId == "new" && !window.socketConnected ){
store.do("load.boards");
}
} catch (e){
window.alert("Error uploading images.");
console.error("Error uploading images: ", e);
} }
window.localStorage.addPinLastBoardId = boardId; } else {
store.do("addPinModal.close");
// if we don't have a listening socket, we need to trigger our own update let postData = {
if ( boardId == "new" && !window.socketConnected ){ boardId: boardId,
store.do("load.boards"); newBoardName: data.addPinModal.newBoardName,
imageUrl: data.addPinModal.imageUrl,
siteUrl: data.addPinModal.siteUrl,
description: data.addPinModal.description
};
let res = await fetch('api/pins', {
method: 'POST',
headers: {
'Content-Type': "application/json"
},
body: JSON.stringify(postData)
});
if ( res.status == 200 ){
let body = await res.json();
if ( data.board && data.board.id == boardId ){
data.board.pins.push(body);
}
window.localStorage.addPinLastBoardId = boardId;
store.do("addPinModal.close");
// if we don't have a listening socket, we need to trigger our own update
if ( boardId == "new" && !window.socketConnected ){
store.do("load.boards");
}
} }
} }
store.do("loader.hide"); store.do("loader.hide");
@ -101,6 +137,54 @@ app.addGetter('addPinModal.isValid', (data) => {
return true; return true;
}); });
app.addSetter('addPinModal.fileChosen', (data, target) => {
let file = target.files[0];
// check type
if ( file.type != "image/jpeg" && file.type != "image/png" ){
window.alert("Unsupported file type. JPEG and PNG images are supported.");
console.log("Unsupported file type: " + file.type);
document.getElementById("fileInput").value = "";
}
// check size
if ( file.size >= 26214400 ){
window.alert("File size exceeds the 25MB limit.");
console.log("File size exceeds the 25MB limit. size=" + file.size);
document.getElementById("fileInput").value = "";
}
else {
let imageUrl = window.URL.createObjectURL(file);
data.addPinModal.uploadFile = file;
data.addPinModal.previewImageUrl = imageUrl;
}
});
app.addSetter('addPinModal.removeUploadFile', (data, target) => {
data.addPinModal.uploadFile = null;
data.addPinModal.previewImageUrl = null;
return false;
});
app.addSetter('addPinModal.disableDidYouKnowDragAndDropMessage', (data) => {
data.addPinModal.didYouKnowDragAndDropMessageDisabled = true;
window.localStorage.addPinModal_didYouKnowDragAndDropMessageDisabled = "true";
});
document.addEventListener("input", (evt) => {
if ( evt.target.id == "fileInput" ){
store.do("addPinModal.fileChosen", evt.target);
}
});
app.addComponent('addPinModal', (store) => { return new Reef("#addPinModal", { app.addComponent('addPinModal', (store) => { return new Reef("#addPinModal", {
store: store, store: store,
@ -109,8 +193,11 @@ app.addComponent('addPinModal', (store) => { return new Reef("#addPinModal", {
let imagePlaceholder = 'data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22300%22%20height%3D%22300%22%3E%3Crect%20x%3D%222%22%20y%3D%222%22%20width%3D%22300%22%20height%3D%22300%22%20style%3D%22fill%3A%23dedede%3B%22%2F%3E%3Ctext%20x%3D%2250%25%22%20y%3D%2250%25%22%20font-size%3D%2218%22%20text-anchor%3D%22middle%22%20alignment-baseline%3D%22middle%22%20font-family%3D%22monospace%2C%20sans-serif%22%20fill%3D%22%23555555%22%3Eimage%3C%2Ftext%3E%3C%2Fsvg%3E'; let imagePlaceholder = 'data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22300%22%20height%3D%22300%22%3E%3Crect%20x%3D%222%22%20y%3D%222%22%20width%3D%22300%22%20height%3D%22300%22%20style%3D%22fill%3A%23dedede%3B%22%2F%3E%3Ctext%20x%3D%2250%25%22%20y%3D%2250%25%22%20font-size%3D%2218%22%20text-anchor%3D%22middle%22%20alignment-baseline%3D%22middle%22%20font-family%3D%22monospace%2C%20sans-serif%22%20fill%3D%22%23555555%22%3Eimage%3C%2Ftext%3E%3C%2Fsvg%3E';
let options = ""; let options = "";
for ( let i = 0; i < data.boards.length; ++i ){ for ( let i = 0; i < data.boards.length; ++i ){
options += `<option value="${data.boards[i].id}">${data.boards[i].name}</option>`; if ( data.showHiddenBoards || !data.boards[i].hidden ){
options += `<option value="${data.boards[i].id}">${data.boards[i].name}</option>`;
}
} }
let newBoardField = ''; let newBoardField = '';
@ -134,6 +221,17 @@ app.addComponent('addPinModal', (store) => { return new Reef("#addPinModal", {
<button class="delete" aria-label="close" data-onclick="addPinModal.close"></button> <button class="delete" aria-label="close" data-onclick="addPinModal.close"></button>
</header> </header>
<section class="modal-card-body"> <section class="modal-card-body">
${ !data.addPinModal.didYouKnowDragAndDropMessageDisabled ? /*html*/`
<div class="message is-success">
<div class="message-header">
<p>Did you know?</p>
<button type="button" class="delete" aria-label="delete" label="Don't show again" data-onclick="addPinModal.disableDidYouKnowDragAndDropMessage"></button>
</div>
<div class="message-body">
Did you know? You can now upload files to an existing board by drag-and-drop!
</div>
</div>
` : ''}
<div class="add-pin-flex"> <div class="add-pin-flex">
<div class="add-pin-flex-left"> <div class="add-pin-flex-left">
<img id="add-pin-modal-img" src="${data.addPinModal.previewImageUrl ? data.addPinModal.previewImageUrl : imagePlaceholder}" /> <img id="add-pin-modal-img" src="${data.addPinModal.previewImageUrl ? data.addPinModal.previewImageUrl : imagePlaceholder}" />
@ -141,6 +239,8 @@ app.addComponent('addPinModal', (store) => { return new Reef("#addPinModal", {
<div class="add-pin-flex-right"> <div class="add-pin-flex-right">
<form> <form>
<div class="field"> <div class="field">
<label class="label">Board</label> <label class="label">Board</label>
<div class="select"> <div class="select">
@ -153,13 +253,31 @@ app.addComponent('addPinModal', (store) => { return new Reef("#addPinModal", {
${newBoardField} ${newBoardField}
${ data.addPinModal.uploadFile ? /*html*/`
<div class="field">
<label class="label">Image File</label>
<div class="control">
<span>${data.addPinModal.uploadFile.name}</span>
<button type="button" class="delete" aria-label="remove" data-onclick="addPinModal.removeUploadFile"></button>
</div>
</div>
` :
/*html*/`
<div class="field"> <div class="field">
<label class="label">Image Url</label> <label class="label">Image Url</label>
<div class="control"> <div class="control">
<input class="input" type="text" data-bind="addPinModal.imageUrl" data-onblur="addPinModal.updatePreview"/> <input class="input" type="text" data-bind="addPinModal.imageUrl" data-onblur="addPinModal.updatePreview" />
</div> </div>
</div> </div>
<div class="field">
<lable class="label">or choose file</lable>
<div class="control">
<input class="input" type="file" id="fileInput" />
</div>
</div>
`}
<div class="field"> <div class="field">
<label class="label">Website Url</label> <label class="label">Website Url</label>
<div class="control"> <div class="control">

View file

@ -30,7 +30,7 @@ app.addGetter('editPinModal.isValid', (data) => {
let pin = getPinById(data.editPinModal.pin.id); let pin = getPinById(data.editPinModal.pin.id);
if ( pin.siteUrl == data.editPinModal.pin.siteUrl && if ( pin && pin.siteUrl == data.editPinModal.pin.siteUrl &&
pin.description == data.editPinModal.pin.description && pin.description == data.editPinModal.pin.description &&
pin.boardId == data.editPinModal.pin.boardId ){ pin.boardId == data.editPinModal.pin.boardId ){
return false; return false;

View file

@ -67,7 +67,7 @@ app.addComponent('navbar', (store) => { return new Reef("#navbar", {
} }
let settingsItem = ""; let settingsItem = "";
if (data.user.admin == 1){ if (data.user && data.user.admin == 1){
settingsItem = ` settingsItem = `
<a class="navbar-item has-text-right" href="./settings"> <a class="navbar-item has-text-right" href="./settings">
<span>tinypin settings</span> <span>tinypin settings</span>

View file

@ -146,6 +146,7 @@ Reef.databind = function(reef){
} else { } else {
elem.checked = false; elem.checked = false;
} }
} else { } else {
elem.value = val; elem.value = val;
} }
@ -170,7 +171,7 @@ Reef.databind = function(reef){
} }
// multiple selects need special handling // multiple selects need special handling
if ( target.tagName == 'SELECT' && target.matches("[multiple]") ){ else if ( target.tagName == 'SELECT' && target.matches("[multiple]") ){
val = []; val = [];
let options = target.querySelectorAll("option"); let options = target.querySelectorAll("option");
for ( let i = 0; i < options.length; ++i ){ for ( let i = 0; i < options.length; ++i ){

View file

@ -58,7 +58,11 @@ function getPinIndexById(id){
} }
function getPinById(id){ function getPinById(id){
return store.data.board.pins[getPinIndexById(id)]; try{
return store.data.board.pins[getPinIndexById(id)];
} catch (e){
return null;
}
} }
async function sleep(ms){ return new Promise((resolve) => setTimeout(resolve, ms)); } async function sleep(ms){ return new Promise((resolve) => setTimeout(resolve, ms)); }

264
package-lock.json generated
View file

@ -14,6 +14,7 @@
"eta": "^1.12.1", "eta": "^1.12.1",
"express": "^4.17.1", "express": "^4.17.1",
"express-ws": "^4.0.0", "express-ws": "^4.0.0",
"multer": "^1.4.3",
"node-fetch": "^2.6.1", "node-fetch": "^2.6.1",
"sharp": "^0.27.0", "sharp": "^0.27.0",
"yargs": "^16.2.0" "yargs": "^16.2.0"
@ -69,6 +70,11 @@
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
}, },
"node_modules/append-field": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz",
"integrity": "sha1-HjRA6RXwsSA9I3SOeO3XubW0PlY="
},
"node_modules/aproba": { "node_modules/aproba": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz",
@ -326,6 +332,44 @@
"ieee754": "^1.1.13" "ieee754": "^1.1.13"
} }
}, },
"node_modules/buffer-from": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="
},
"node_modules/busboy": {
"version": "0.2.14",
"resolved": "https://registry.npmjs.org/busboy/-/busboy-0.2.14.tgz",
"integrity": "sha1-bCpiLvz0fFe7vh4qnDetNseSVFM=",
"dependencies": {
"dicer": "0.2.5",
"readable-stream": "1.1.x"
},
"engines": {
"node": ">=0.8.0"
}
},
"node_modules/busboy/node_modules/isarray": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
"integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8="
},
"node_modules/busboy/node_modules/readable-stream": {
"version": "1.1.14",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz",
"integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=",
"dependencies": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.1",
"isarray": "0.0.1",
"string_decoder": "~0.10.x"
}
},
"node_modules/busboy/node_modules/string_decoder": {
"version": "0.10.31",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
"integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ="
},
"node_modules/bytes": { "node_modules/bytes": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
@ -428,6 +472,20 @@
"simple-swizzle": "^0.2.2" "simple-swizzle": "^0.2.2"
} }
}, },
"node_modules/concat-stream": {
"version": "1.6.2",
"resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz",
"integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==",
"engines": [
"node >= 0.8"
],
"dependencies": {
"buffer-from": "^1.0.0",
"inherits": "^2.0.3",
"readable-stream": "^2.2.2",
"typedarray": "^0.0.6"
}
},
"node_modules/console-control-strings": { "node_modules/console-control-strings": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
@ -541,6 +599,39 @@
"node": ">=0.10" "node": ">=0.10"
} }
}, },
"node_modules/dicer": {
"version": "0.2.5",
"resolved": "https://registry.npmjs.org/dicer/-/dicer-0.2.5.tgz",
"integrity": "sha1-WZbAhrszIYyBLAkL3cCc0S+stw8=",
"dependencies": {
"readable-stream": "1.1.x",
"streamsearch": "0.1.2"
},
"engines": {
"node": ">=0.8.0"
}
},
"node_modules/dicer/node_modules/isarray": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
"integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8="
},
"node_modules/dicer/node_modules/readable-stream": {
"version": "1.1.14",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz",
"integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=",
"dependencies": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.1",
"isarray": "0.0.1",
"string_decoder": "~0.10.x"
}
},
"node_modules/dicer/node_modules/string_decoder": {
"version": "0.10.31",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
"integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ="
},
"node_modules/ee-first": { "node_modules/ee-first": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@ -909,6 +1000,17 @@
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
}, },
"node_modules/mkdirp": {
"version": "0.5.5",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
"integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
"dependencies": {
"minimist": "^1.2.5"
},
"bin": {
"mkdirp": "bin/cmd.js"
}
},
"node_modules/mkdirp-classic": { "node_modules/mkdirp-classic": {
"version": "0.5.3", "version": "0.5.3",
"resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
@ -919,6 +1021,24 @@
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
}, },
"node_modules/multer": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/multer/-/multer-1.4.3.tgz",
"integrity": "sha512-np0YLKncuZoTzufbkM6wEKp68EhWJXcU6fq6QqrSwkckd2LlMgd1UqhUJLj6NS/5sZ8dE8LYDWslsltJznnXlg==",
"dependencies": {
"append-field": "^1.0.0",
"busboy": "^0.2.11",
"concat-stream": "^1.5.2",
"mkdirp": "^0.5.4",
"object-assign": "^4.1.1",
"on-finished": "^2.3.0",
"type-is": "^1.6.4",
"xtend": "^4.0.0"
},
"engines": {
"node": ">= 0.10.0"
}
},
"node_modules/napi-build-utils": { "node_modules/napi-build-utils": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz",
@ -1341,6 +1461,14 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/streamsearch": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz",
"integrity": "sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo=",
"engines": {
"node": ">=0.8.0"
}
},
"node_modules/string_decoder": { "node_modules/string_decoder": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
@ -1451,6 +1579,11 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/typedarray": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
"integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c="
},
"node_modules/unpipe": { "node_modules/unpipe": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
@ -1562,6 +1695,14 @@
"async-limiter": "~1.0.0" "async-limiter": "~1.0.0"
} }
}, },
"node_modules/xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
"engines": {
"node": ">=0.4"
}
},
"node_modules/y18n": { "node_modules/y18n": {
"version": "5.0.5", "version": "5.0.5",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.5.tgz", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.5.tgz",
@ -1679,6 +1820,11 @@
} }
} }
}, },
"append-field": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz",
"integrity": "sha1-HjRA6RXwsSA9I3SOeO3XubW0PlY="
},
"aproba": { "aproba": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz",
@ -1869,6 +2015,43 @@
"ieee754": "^1.1.13" "ieee754": "^1.1.13"
} }
}, },
"buffer-from": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="
},
"busboy": {
"version": "0.2.14",
"resolved": "https://registry.npmjs.org/busboy/-/busboy-0.2.14.tgz",
"integrity": "sha1-bCpiLvz0fFe7vh4qnDetNseSVFM=",
"requires": {
"dicer": "0.2.5",
"readable-stream": "1.1.x"
},
"dependencies": {
"isarray": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
"integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8="
},
"readable-stream": {
"version": "1.1.14",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz",
"integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=",
"requires": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.1",
"isarray": "0.0.1",
"string_decoder": "~0.10.x"
}
},
"string_decoder": {
"version": "0.10.31",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
"integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ="
}
}
},
"bytes": { "bytes": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
@ -1955,6 +2138,17 @@
"simple-swizzle": "^0.2.2" "simple-swizzle": "^0.2.2"
} }
}, },
"concat-stream": {
"version": "1.6.2",
"resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz",
"integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==",
"requires": {
"buffer-from": "^1.0.0",
"inherits": "^2.0.3",
"readable-stream": "^2.2.2",
"typedarray": "^0.0.6"
}
},
"console-control-strings": { "console-control-strings": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
@ -2038,6 +2232,38 @@
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
"integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=" "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups="
}, },
"dicer": {
"version": "0.2.5",
"resolved": "https://registry.npmjs.org/dicer/-/dicer-0.2.5.tgz",
"integrity": "sha1-WZbAhrszIYyBLAkL3cCc0S+stw8=",
"requires": {
"readable-stream": "1.1.x",
"streamsearch": "0.1.2"
},
"dependencies": {
"isarray": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
"integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8="
},
"readable-stream": {
"version": "1.1.14",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz",
"integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=",
"requires": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.1",
"isarray": "0.0.1",
"string_decoder": "~0.10.x"
}
},
"string_decoder": {
"version": "0.10.31",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
"integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ="
}
}
},
"ee-first": { "ee-first": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@ -2318,6 +2544,14 @@
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
}, },
"mkdirp": {
"version": "0.5.5",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
"integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
"requires": {
"minimist": "^1.2.5"
}
},
"mkdirp-classic": { "mkdirp-classic": {
"version": "0.5.3", "version": "0.5.3",
"resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
@ -2328,6 +2562,21 @@
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
}, },
"multer": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/multer/-/multer-1.4.3.tgz",
"integrity": "sha512-np0YLKncuZoTzufbkM6wEKp68EhWJXcU6fq6QqrSwkckd2LlMgd1UqhUJLj6NS/5sZ8dE8LYDWslsltJznnXlg==",
"requires": {
"append-field": "^1.0.0",
"busboy": "^0.2.11",
"concat-stream": "^1.5.2",
"mkdirp": "^0.5.4",
"object-assign": "^4.1.1",
"on-finished": "^2.3.0",
"type-is": "^1.6.4",
"xtend": "^4.0.0"
}
},
"napi-build-utils": { "napi-build-utils": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz",
@ -2652,6 +2901,11 @@
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
"integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow="
}, },
"streamsearch": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz",
"integrity": "sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo="
},
"string_decoder": { "string_decoder": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
@ -2740,6 +2994,11 @@
"mime-types": "~2.1.24" "mime-types": "~2.1.24"
} }
}, },
"typedarray": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
"integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c="
},
"unpipe": { "unpipe": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
@ -2826,6 +3085,11 @@
"async-limiter": "~1.0.0" "async-limiter": "~1.0.0"
} }
}, },
"xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="
},
"y18n": { "y18n": {
"version": "5.0.5", "version": "5.0.5",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.5.tgz", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.5.tgz",

View file

@ -15,6 +15,7 @@
"eta": "^1.12.1", "eta": "^1.12.1",
"express": "^4.17.1", "express": "^4.17.1",
"express-ws": "^4.0.0", "express-ws": "^4.0.0",
"multer": "^1.4.3",
"node-fetch": "^2.6.1", "node-fetch": "^2.6.1",
"sharp": "^0.27.0", "sharp": "^0.27.0",
"yargs": "^16.2.0" "yargs": "^16.2.0"

View file

@ -8,6 +8,36 @@ function sendAuthCookie(res, c){
res.cookie('s', tokenUtils.encrypt(c), {maxAge: 315569520000}); // 10 years res.cookie('s', tokenUtils.encrypt(c), {maxAge: 315569520000}); // 10 years
} }
function maybeGetUser(req){
if ( !req.cookies ){
return null;
}
// if we made it this far, we're eady to check for the cookie
let s = req.cookies.s;
// TODO: should probably check if the user's access has been revoked,
// but we currently don't allow deleting users anyway. A key rotation would
// be the other solution, but that would log out all users and require new tokens
// to be created.
if ( s ){
try {
s = tokenUtils.decrypt(s);
if ( s.i && s.u ){
return {
id: s.i,
name: s.u
}
}
} catch (err) {
console.log(`error parsing cookie: `, err);
}
}
return null;
}
module.exports = async (req, res, next) => { module.exports = async (req, res, next) => {
// we will also accept the auth token in the x-api-key header // we will also accept the auth token in the x-api-key header
@ -60,6 +90,13 @@ module.exports = async (req, res, next) => {
next(); next();
return; return;
} if ( req.method == "GET" && req.originalUrl == "/login" ){ } if ( req.method == "GET" && req.originalUrl == "/login" ){
if ( maybeGetUser(req) ){
res.redirect("./");
return;
}
console.log("login"); console.log("login");
// res.type("html").sendFile(path.resolve('./templates/login.html')); // res.type("html").sendFile(path.resolve('./templates/login.html'));
res.render("login", { registerEnabled: dao.getProperty("registerEnabled") }); res.render("login", { registerEnabled: dao.getProperty("registerEnabled") });
@ -135,26 +172,27 @@ module.exports = async (req, res, next) => {
return; return;
} }
// if we made it this far, we're eady to check for the cookie // // if we made it this far, we're eady to check for the cookie
let s = req.cookies.s; // let s = req.cookies.s;
// TODO: should probably check if the user's access has been revoked, // // TODO: should probably check if the user's access has been revoked,
// but we currently don't allow deleting users anyway. A key rotation would // // but we currently don't allow deleting users anyway. A key rotation would
// be the other solution, but that would log out all users and require new tokens // // be the other solution, but that would log out all users and require new tokens
// to be created. // // to be created.
if ( s ){ // if ( s ){
try { // try {
s = tokenUtils.decrypt(s); // s = tokenUtils.decrypt(s);
if ( s.i && s.u ){ // if ( s.i && s.u ){
req.user = { // req.user = {
id: s.i, // id: s.i,
name: s.u // name: s.u
} // }
} // }
} catch (err) { // } catch (err) {
console.error(`error parsing cookie: `, err); // console.error(`error parsing cookie: `, err);
} // }
} // }
req.user = maybeGetUser(req);
if ( !req.user ){ if ( !req.user ){
res.redirect("/login"); res.redirect("/login");

View file

@ -1,6 +1,7 @@
const yargs = require('yargs'); const yargs = require('yargs');
const express = require('express'); const express = require('express');
const bodyParser = require('body-parser'); const bodyParser = require('body-parser');
const multer = require("multer")
const path = require('path'); const path = require('path');
const cookieParser = require('cookie-parser'); const cookieParser = require('cookie-parser');
const tokenUtil = require('./token-utils.js'); const tokenUtil = require('./token-utils.js');
@ -10,6 +11,9 @@ const imageUtils = require('./image-utils.js');
var eta = require("eta"); var eta = require("eta");
const tokenUtils = require('./token-utils.js'); const tokenUtils = require('./token-utils.js');
// consider using temp files, but we're going to limit the size so should be ok
const upload = multer({storage:multer.memoryStorage(), limits: {fileSize: 26214400, files: 1}}); // 1 - 25MB file
module.exports = async () => { module.exports = async () => {
process.on('SIGINT', () => { process.on('SIGINT', () => {
@ -340,6 +344,7 @@ module.exports = async () => {
res.status(200).send({t: token}); res.status(200).send({t: token});
}); });
// handle raw uploads for pin creation
app.post("/up", async (req, res) => { app.post("/up", async (req, res) => {
try { try {
@ -358,7 +363,7 @@ module.exports = async () => {
board = dao.createBoard(req.user.id, boardName, 0); board = dao.createBoard(req.user.id, boardName, 0);
} }
let pin = dao.createPin(req.user.id, board.id, null, null, null, null, image.original.height, image.original.width, image.thumbnail.height, image.thumbnailWidth); let pin = dao.createPin(req.user.id, board.id, null, null, null, null, image.original.height, image.original.width, image.thumbnail.height, image.thumbnail.height);
await imageUtils.saveImage(req.user.id, pin.id, image); await imageUtils.saveImage(req.user.id, pin.id, image);
@ -371,6 +376,38 @@ module.exports = async () => {
} }
}); });
// handle multipart uploads for pin creation
app.post("/multiup", upload.single('file'), async(req, res) => {
try {
let image = await imageUtils.processImage(req.file.buffer); // file.buffer only works with the Memory store for multer.
let boardId = req.body.boardId;
let board = null;
if ( boardId == "new" ){
board = dao.createBoard(req.user.id, req.body.newBoardName, 0);
} else {
board = dao.getBoard(req.user.id, boardId);
}
console.log(image);
let pin = dao.createPin(req.user.id, board.id, null, req.body.siteUrl, req.body.description, null, image.original.height, image.original.width, image.thumbnail.height, image.thumbnail.height);
await imageUtils.saveImage(req.user.id, pin.id, image);
broadcast(req.user.id, {updateBoard:board.id});
res.status(200).send(pin);
} catch (err) {
console.log(`Error creating pin via multipart upload`, err);
res.status(500).send(SERVER_ERROR);
}
});
app.get("/api/apikey", (req,res) => { app.get("/api/apikey", (req,res) => {
let s = req.cookies['s']; let s = req.cookies['s'];
console.log("s=" + s); console.log("s=" + s);