Merge branch 'webdav'

This commit is contained in:
raphael 2019-08-19 10:29:04 +02:00
commit ed41ca9edd
8 changed files with 4433 additions and 38 deletions

View File

@ -12,6 +12,7 @@ This is a lightweight NodeJS collaborative Whiteboard/Sketchboard witch can easi
* Save Whiteboard to Image and JSON * Save Whiteboard to Image and JSON
* Draw angle lines by pressing "shift" while drawing (with line tool) * Draw angle lines by pressing "shift" while drawing (with line tool)
* Draw square by pressing "shift" while drawing (with rectangle tool) * Draw square by pressing "shift" while drawing (with rectangle tool)
* Indicator that shows the smallest screen participating
* Working on PC, Tablet & Mobile * Working on PC, Tablet & Mobile
## Install the App ## Install the App
@ -25,7 +26,7 @@ You can run this app with and without docker
### With Docker ### With Docker
1. `docker run -d -p 8080:8080 rofl256/whiteboard` 1. `docker run -d -p 8080:8080 rofl256/whiteboard`
2. Surf to http://YOURIP:8080 2. Surf to http://YOURIP:8080s
## URL Parameters ## URL Parameters
Call your site with GET parameters to change the WhiteboardID or the Username Call your site with GET parameters to change the WhiteboardID or the Username
@ -38,27 +39,45 @@ Call your site with GET parameters to change the WhiteboardID or the Username
## Security - AccessToken (Optional) ## Security - AccessToken (Optional)
To prevent clients who might know or guess the base URL from abusing the server to upload files and stuff..., you can set an accesstoken at server start. To prevent clients who might know or guess the base URL from abusing the server to upload files and stuff..., you can set an accesstoken at server start.
<b>Without docker:</b> `node server.js --accesstoken="mySecToken"` <b>Server (Without docker):</b> `node server.js --accesstoken="mySecToken"`
<b>With docker:</b> `docker run -d -e accesstoken="mySecToken" -p 8080:8080 rofl256/whiteboard` <b>Server (With docker):</b> `docker run -d -e accesstoken="mySecToken" -p 8080:8080 rofl256/whiteboard`
Then set the same token on the client side as well. Then set the same token on the client side as well:
<b>Client (With and without docker):</b> `http://YOURIP:8080?accesstoken=mySecToken&whiteboardid=MYID&username=MYNAME` <b>Client (With and without docker):</b> `http://YOURIP:8080?accesstoken=mySecToken&whiteboardid=MYID&username=MYNAME`
Done! Done!
## WebDAV (Optional)
This function allows your users to save the whiteboard directly to a webdav server as image without downloading it.
<b>Server (Without docker):</b> `node server.js --webdav=true`
<b>Server (With docker):</b> `docker run -d -e webdav=true -p 8080:8080 rofl256/whiteboard`
Then set the same parameter on the client side as well:
<b>Client (With and without docker):</b> `http://YOURIP:8080?webdav=true&whiteboardid=MYID&username=MYNAME`
Refresh the site and You will notice an extra save button in the top panel. Set your WebDav Parameters, and you are good to go!
Note: For the most owncloud/nextcloud setups you have to set the Server URL to: https://YourDomain.tl/remote.php/webdav/
Done!
## Things you may want to know ## Things you may want to know
* Whiteboards are gone if you restart the Server, so keep that in mind (or save your whiteboard) * Whiteboards are gone if you restart the Server, so keep that in mind (or save your whiteboard)
* You shoud be able to customize the layout without ever toutching the whiteboard.js (take a look at index.html & main.js) * You shoud be able to customize the layout without ever toutching the whiteboard.js (take a look at index.html & main.js)
## All server run parameters (also docker) ## All server start parameters (also docker)
* accesstoken => take a look at "Security - AccessToken" for a full explanation * accesstoken => take a look at "Security - AccessToken" for a full explanation
* disablesmallestscreen => set this to "true" if you don't want show the "smallest screen" indicator (A dotted gray line) to the users * disablesmallestscreen => set this to "true" if you don't want show the "smallest screen" indicator (A dotted gray line) to the users
* webdav => Enable the function to save to a webdav-server (Must also be enabled on the client; Take a look at the webdav section)
## ToDo ## ToDo
* Make undo function more reliable on texts * Make undo function more reliable on texts
* Add more callbacks for errors and things ...
## Nginx Reverse Proxy configuration ## Nginx Reverse Proxy configuration
Add this to your server part: Add this to your server part:

4172
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,5 @@
{ {
"name": "Whiteboard", "name": "whiteboard",
"version": "1.0.0", "version": "1.0.0",
"description": "Collaborative Whiteboard / Sketchboard", "description": "Collaborative Whiteboard / Sketchboard",
"main": "server.js", "main": "server.js",
@ -23,7 +23,8 @@
"formidable": "1.*", "formidable": "1.*",
"fs-extra": "7.*", "fs-extra": "7.*",
"jsdom": "^14.0.0", "jsdom": "^14.0.0",
"socket.io": "2.*" "socket.io": "2.*",
"webdav": "^2.8.0"
}, },
"author": "Cracker0dks", "author": "Cracker0dks",
"license": "MIT" "license": "MIT"

View File

@ -74,4 +74,12 @@ button {
.textBox.active>.removeIcon, .textBox.active>.moveIcon { .textBox.active>.removeIcon, .textBox.active>.moveIcon {
display: block; display: block;
}
.modalBtn {
padding: 5px;
border-radius: 5px;
border: 0px;
min-width: 50px;
cursor: pointer;
} }

View File

@ -99,6 +99,13 @@
<i style="position: absolute; top: 3px; left: 2px; color: #000000; font-size: 0.5em; " <i style="position: absolute; top: 3px; left: 2px; color: #000000; font-size: 0.5em; "
class="fas fa-save"></i> class="fas fa-save"></i>
</button> </button>
<button style="position: relative; display: none;" id="uploadWebDavBtn" title="Save whiteboard to webdav"
type="button" class="whiteboardBtn">
<i class="fas fa-globe"></i>
<i style="position: absolute; top: 3px; left: 2px; color: #000000; font-size: 0.5em; "
class="fas fa-save"></i>
</button>
</div> </div>
<div class="btn-group"> <div class="btn-group">

View File

@ -31,7 +31,7 @@ signaling_socket.on('connect', function () {
}); });
signaling_socket.on('wrongAccessToken', function () { signaling_socket.on('wrongAccessToken', function () {
if(!accessDenied) { if (!accessDenied) {
accessDenied = true; accessDenied = true;
showBasicAlert("Access denied! Wrong accessToken!") showBasicAlert("Access denied! Wrong accessToken!")
} }
@ -45,6 +45,10 @@ signaling_socket.on('connect', function () {
}); });
$(document).ready(function () { $(document).ready(function () {
if (getQueryVariable("webdav") == "true") {
$("#uploadWebDavBtn").show();
}
whiteboard.loadWhiteboard("#whiteboardContainer", { //Load the whiteboard whiteboard.loadWhiteboard("#whiteboardContainer", { //Load the whiteboard
whiteboardId: whiteboardId, whiteboardId: whiteboardId,
username: myUsername, username: myUsername,
@ -164,6 +168,80 @@ $(document).ready(function () {
}, 0); }, 0);
}); });
$("#uploadWebDavBtn").click(function () {
if ($(".webdavUploadBtn").length > 0) {
return;
}
var webdavserver = localStorage.getItem('webdavserver') || ""
var webdavpath = localStorage.getItem('webdavpath') || "/"
var webdavusername = localStorage.getItem('webdavusername') || ""
var webdavpassword =localStorage.getItem('webdavpassword') || ""
var webDavHtml = $('<div>' +
'<table>' +
'<tr>' +
'<td>Server URL:</td>' +
'<td><input class="webdavserver" type="text" value="'+webdavserver+'" placeholder="https://yourserver.com/remote.php/webdav/"></td>' +
'<td></td>' +
'</tr>' +
'<tr>' +
'<td>Path:</td>' +
'<td><input class="webdavpath" type="text" placeholder="folder" value="'+webdavpath+'"></td>' +
'<td style="font-size: 0.7em;"><i>path always have to start & end with "/"</i></td>' +
'</tr>' +
'<tr>' +
'<td>Username:</td>' +
'<td><input class="webdavusername" type="text" value="'+webdavusername+'" placeholder="username"></td>' +
'<td style="font-size: 0.7em;"></td>' +
'</tr>' +
'<tr>' +
'<td>Password:</td>' +
'<td><input class="webdavpassword" type="password" value="'+webdavpassword+'" placeholder="password"></td>' +
'<td style="font-size: 0.7em;"></td>' +
'</tr>' +
'<tr>' +
'<td style="font-size: 0.7em;" colspan="3">Note: You have to generate and use app credentials if you have 2 Factor Auth activated on your dav/nextcloud server!</td>' +
'</tr>' +
'<tr>' +
'<td></td>' +
'<td colspan="2"><span class="loadingWebdavText" style="display:none;">Saving to webdav, please wait...</span><button class="modalBtn webdavUploadBtn"><i class="fas fa-upload"></i> Start Upload</button></td>' +
'</tr>' +
'</table>' +
'</div>');
webDavHtml.find(".webdavUploadBtn").click(function () {
var webdavserver = webDavHtml.find(".webdavserver").val();
localStorage.setItem('webdavserver', webdavserver);
var webdavpath = webDavHtml.find(".webdavpath").val();
localStorage.setItem('webdavpath', webdavpath);
var webdavusername = webDavHtml.find(".webdavusername").val();
localStorage.setItem('webdavusername', webdavusername);
var webdavpassword = webDavHtml.find(".webdavpassword").val();
localStorage.setItem('webdavpassword', webdavpassword);
var base64data = whiteboard.getImageDataBase64();
var webdavaccess = {
webdavserver: webdavserver,
webdavpath: webdavpath,
webdavusername: webdavusername,
webdavpassword: webdavpassword
}
webDavHtml.find(".loadingWebdavText").show();
webDavHtml.find(".webdavUploadBtn").hide();
saveWhiteboardToWebdav(base64data, webdavaccess, function (err) {
if (err) {
webDavHtml.find(".loadingWebdavText").hide();
webDavHtml.find(".webdavUploadBtn").show();
} else {
webDavHtml.parents(".basicalert").remove();
}
});
})
showBasicAlert(webDavHtml, {
header: "Save to Webdav",
okBtnText: "cancel",
headercolor: "#0082c9"
})
});
// upload json containing steps // upload json containing steps
$("#uploadJsonBtn").click(function () { $("#uploadJsonBtn").click(function () {
$("#myFile").click(); $("#myFile").click();
@ -257,7 +335,7 @@ $(document).ready(function () {
showBasicAlert("File must be an image!"); showBasicAlert("File must be an image!");
} }
} else { //File from other browser } else { //File from other browser
var fileUrl = e.originalEvent.dataTransfer.getData('URL'); var fileUrl = e.originalEvent.dataTransfer.getData('URL');
var imageUrl = e.originalEvent.dataTransfer.getData('text/html'); var imageUrl = e.originalEvent.dataTransfer.getData('text/html');
var rex = /src="?([^"\s]+)"?\s*/; var rex = /src="?([^"\s]+)"?\s*/;
@ -330,6 +408,36 @@ function uploadImgAndAddToWhiteboard(base64data) {
}); });
} }
function saveWhiteboardToWebdav(base64data, webdavaccess, callback) {
var date = (+new Date());
$.ajax({
type: 'POST',
url: document.URL.substr(0, document.URL.lastIndexOf('/')) + '/upload',
data: {
'imagedata': base64data,
'whiteboardId': whiteboardId,
'date': date,
'at': accessToken,
'webdavaccess': JSON.stringify(webdavaccess)
},
success: function (msg) {
showBasicAlert("Whiteboard was saved to Webdav!", {
headercolor: "#5c9e5c"
});
console.log("Image uploaded for webdav!");
callback();
},
error: function (err) {
if (err.status == 403) {
showBasicAlert("Could not connect to Webdav folder! Please check the credentials and paths and try again!");
} else {
showBasicAlert("Unknown Webdav error! ", err);
}
callback(err);
}
});
}
// verify if filename refers to an image // verify if filename refers to an image
function isImageFileName(filename) { function isImageFileName(filename) {
var extension = filename.split(".")[filename.split(".").length - 1]; var extension = filename.split(".")[filename.split(".").length - 1];
@ -357,6 +465,9 @@ function isValidImageUrl(url, callback) {
// handle pasting from clipboard // handle pasting from clipboard
window.addEventListener("paste", function (e) { window.addEventListener("paste", function (e) {
if($(".basicalert").length>0) {
return;
}
if (e.clipboardData) { if (e.clipboardData) {
var items = e.clipboardData.items; var items = e.clipboardData.items;
var imgItemFound = false; var imgItemFound = false;
@ -379,22 +490,33 @@ window.addEventListener("paste", function (e) {
} }
} }
if (!imgItemFound && whiteboard.tool!="text") { if (!imgItemFound && whiteboard.tool != "text") {
showBasicAlert("Please Drag&Drop the image into the Whiteboard. (Browsers don't allow copy+past from the filesystem directly)"); showBasicAlert("Please Drag&Drop the image into the Whiteboard. (Browsers don't allow copy+past from the filesystem directly)");
} }
} }
}); });
function showBasicAlert(text) { function showBasicAlert(html, newOptions) {
var alertHtml = $('<div style="position:absolute; top:0px; left:0px; width:100%; top:70px; font-family: monospace;">' + var options = {
'<div style="width: 30%; margin: auto; background: #aaaaaa; border-radius: 5px; font-size: 1.2em; border: 1px solid gray;">'+ header: "INFO MESSAGE",
'<div style="border-bottom: 1px solid #676767; background: #d25d5d; padding-left: 5px; font-size: 0.8em;">INFO MESSAGE</div>'+ okBtnText: "Ok",
'<div style="padding: 10px;">' + text + '</div>'+ headercolor: "#d25d5d"
'<div style="height: 20px; padding: 10px;"><button style="float: right; padding: 5px; border-radius: 5px; border: 0px; min-width: 50px; cursor: pointer;">Ok</button></div>'+ }
if (newOptions) {
for (var i in newOptions) {
options[i] = newOptions[i];
}
}
var alertHtml = $('<div class="basicalert" style="position:absolute; top:0px; left:0px; width:100%; top:70px; font-family: monospace;">' +
'<div style="width: 30%; margin: auto; background: #aaaaaa; border-radius: 5px; font-size: 1.2em; border: 1px solid gray;">' +
'<div style="border-bottom: 1px solid #676767; background: ' + options["headercolor"] + '; padding-left: 5px; font-size: 0.8em;">' + options["header"] + '</div>' +
'<div style="padding: 10px;" class="htmlcontent"></div>' +
'<div style="height: 20px; padding: 10px;"><button class="modalBtn okbtn" style="float: right;">' + options["okBtnText"] + '</button></div>' +
'</div>' + '</div>' +
'</div>'); '</div>');
alertHtml.find(".htmlcontent").append(html);
$("body").append(alertHtml); $("body").append(alertHtml);
alertHtml.find("button").click(function () { alertHtml.find(".okbtn").click(function () {
alertHtml.remove(); alertHtml.remove();
}) })
} }

View File

@ -298,12 +298,9 @@ var whiteboard = {
imgDiv.find(".addToCanvasBtn").click(function () { imgDiv.find(".addToCanvasBtn").click(function () {
_this.imgDragActive = false; _this.imgDragActive = false;
_this.refreshCursorAppearance(); _this.refreshCursorAppearance();
var widthT = imgDiv.width();
var heightT = imgDiv.height();
var p = imgDiv.position(); var p = imgDiv.position();
var leftT = Math.round(p.left * 100) / 100; var leftT = Math.round(p.left * 100) / 100;
var topT = Math.round(p.top * 100) / 100; var topT = Math.round(p.top * 100) / 100;
//xf, yf, xt, yt, width, height
_this.drawId++; _this.drawId++;
_this.sendFunction({ "t": _this.tool, "d": [left, top, leftT, topT, width, height] }); _this.sendFunction({ "t": _this.tool, "d": [left, top, leftT, topT, width, height] });
_this.dragCanvasRectContent(left, top, leftT, topT, width, height); _this.dragCanvasRectContent(left, top, leftT, topT, width, height);
@ -774,7 +771,6 @@ var whiteboard = {
var left = Math.round(p.left * 100) / 100; var left = Math.round(p.left * 100) / 100;
var top = Math.round(p.top * 100) / 100; var top = Math.round(p.top * 100) / 100;
top += 25; //Fix top position top += 25; //Fix top position
console.log(text, fontSize, fontColor, left, top)
ctx.font = fontSize + " monospace"; ctx.font = fontSize + " monospace";
ctx.fillStyle = fontColor; ctx.fillStyle = fontColor;
ctx.fillText(text, left, top); ctx.fillText(text, left, top);
@ -801,7 +797,7 @@ var whiteboard = {
} }
}); });
}, },
loadDataInSteps(content, isNewData, callAfterEveryStep, doneCallback) { loadDataInSteps(content, isNewData, callAfterEveryStep) {
var _this = this; var _this = this;
function lData(index) { function lData(index) {
for (var i = index; i < content.length; i++) { for (var i = index; i < content.length; i++) {
@ -819,12 +815,15 @@ var whiteboard = {
} }
lData(0); lData(0);
}, },
loadJsonData(content) { loadJsonData(content, doneCallback) {
var _this = this; var _this = this;
_this.loadDataInSteps(content, false, function (stepData, index) { _this.loadDataInSteps(content, false, function (stepData, index) {
_this.sendFunction(stepData); _this.sendFunction(stepData);
if (index >= content.length - 1) { //Done with all data if (index >= content.length - 1) { //Done with all data
_this.drawId++; _this.drawId++;
if(doneCallback) {
doneCallback();
}
} }
}); });
}, },
@ -842,12 +841,6 @@ var whiteboard = {
_this.drawBuffer.push(content); _this.drawBuffer.push(content);
} }
}, },
isRecRecCollision: function (rx1, ry1, rw1, rh1, rx2, ry2, rw2, rh2) {
return rx1 < rx2 + rw2 && rx1 + rw1 > rx2 && ry1 < ry2 + rh2 && rh1 + ry1 > ry2;
},
isRecPointCollision: function (rx, ry, rw, rh, px, py) {
return rx <= px && px <= rx + rw && ry <= py && py <= ry + rh;
},
refreshCursorAppearance() { //Set cursor depending on current active tool refreshCursorAppearance() { //Set cursor depending on current active tool
var _this = this; var _this = this;
if (_this.tool === "pen" || _this.tool === "eraser") { if (_this.tool === "pen" || _this.tool === "eraser") {

View File

@ -1,6 +1,7 @@
var PORT = 8080; //Set port for the app var PORT = 8080; //Set port for the app
var accessToken = ""; //Can be set here or as start parameter (node server.js --accesstoken=MYTOKEN) var accessToken = ""; //Can be set here or as start parameter (node server.js --accesstoken=MYTOKEN)
var disableSmallestScreen = false; //Can be set to true if you dont want to show (node server.js --disablesmallestscreen=true) var disableSmallestScreen = false; //Can be set to true if you dont want to show (node server.js --disablesmallestscreen=true)
var webdav = false; //Can be set to true if you want to allow webdav save (node server.js --webdav=true)
var fs = require("fs-extra"); var fs = require("fs-extra");
var express = require('express'); var express = require('express');
@ -11,6 +12,8 @@ const { JSDOM } = require('jsdom');
const window = (new JSDOM('')).window; const window = (new JSDOM('')).window;
const DOMPurify = createDOMPurify(window); const DOMPurify = createDOMPurify(window);
const { createClient } = require("webdav");
var s_whiteboard = require("./s_whiteboard.js"); var s_whiteboard = require("./s_whiteboard.js");
var app = express(); var app = express();
@ -26,6 +29,9 @@ if (process.env.accesstoken) {
if (process.env.disablesmallestscreen) { if (process.env.disablesmallestscreen) {
disablesmallestscreen = true; disablesmallestscreen = true;
} }
if (process.env.webdav) {
webdav = true;
}
var startArgs = getArgs(); var startArgs = getArgs();
if (startArgs["accesstoken"]) { if (startArgs["accesstoken"]) {
@ -34,6 +40,9 @@ if (startArgs["accesstoken"]) {
if (startArgs["disablesmallestscreen"]) { if (startArgs["disablesmallestscreen"]) {
disableSmallestScreen = true; disableSmallestScreen = true;
} }
if (startArgs["webdav"]) {
webdav = true;
}
if (accessToken !== "") { if (accessToken !== "") {
console.log("AccessToken set to: " + accessToken); console.log("AccessToken set to: " + accessToken);
@ -41,6 +50,9 @@ if (accessToken !== "") {
if (disableSmallestScreen) { if (disableSmallestScreen) {
console.log("Disabled showing smallest screen resolution!"); console.log("Disabled showing smallest screen resolution!");
} }
if (webdav) {
console.log("Webdav save is enabled!");
}
app.get('/loadwhiteboard', function (req, res) { app.get('/loadwhiteboard', function (req, res) {
var wid = req["query"]["wid"]; var wid = req["query"]["wid"];
@ -76,8 +88,18 @@ app.post('/upload', function (req, res) { //File upload
form.on('end', function () { form.on('end', function () {
if (accessToken === "" || accessToken == formData["fields"]["at"]) { if (accessToken === "" || accessToken == formData["fields"]["at"]) {
progressUploadFormData(formData); progressUploadFormData(formData, function (err) {
res.send("done"); if (err) {
if (err == "403") {
res.status(403);
} else {
res.status(500);
}
res.end();
} else {
res.send("done");
}
});
} else { } else {
res.status(401); //Unauthorized res.status(401); //Unauthorized
res.end(); res.end();
@ -87,7 +109,7 @@ app.post('/upload', function (req, res) { //File upload
form.parse(req); form.parse(req);
}); });
function progressUploadFormData(formData) { function progressUploadFormData(formData, callback) {
console.log("Progress new Form Data"); console.log("Progress new Form Data");
var fields = escapeAllContentStrings(formData.fields); var fields = escapeAllContentStrings(formData.fields);
var files = formData.files; var files = formData.files;
@ -96,7 +118,12 @@ function progressUploadFormData(formData) {
var name = fields["name"] || ""; var name = fields["name"] || "";
var date = fields["date"] || (+new Date()); var date = fields["date"] || (+new Date());
var filename = whiteboardId + "_" + date + ".png"; var filename = whiteboardId + "_" + date + ".png";
var webdavaccess = fields["webdavaccess"] || false;
try {
webdavaccess = JSON.parse(webdavaccess);
} catch (e) {
webdavaccess = false;
}
fs.ensureDir("./public/uploads", function (err) { fs.ensureDir("./public/uploads", function (err) {
if (err) { if (err) {
console.log("Could not create upload folder!", err); console.log("Could not create upload folder!", err);
@ -109,14 +136,61 @@ function progressUploadFormData(formData) {
fs.writeFile('./public/uploads/' + filename, imagedata, 'base64', function (err) { fs.writeFile('./public/uploads/' + filename, imagedata, 'base64', function (err) {
if (err) { if (err) {
console.log("error", err); console.log("error", err);
callback(err);
} else {
if (webdavaccess) { //Save image to webdav
if (webdav) {
saveImageToWebdav('./public/uploads/' + filename, filename, webdavaccess, function (err) {
if (err) {
console.log("error", err);
callback(err);
} else {
callback();
}
})
} else {
callback("Webdav is not enabled on the server!");
}
} else {
callback();
}
} }
}); });
} else { } else {
callback("no imagedata!");
console.log("No image Data found for this upload!", name); console.log("No image Data found for this upload!", name);
} }
}); });
} }
function saveImageToWebdav(imagepath, filename, webdavaccess, callback) {
if (webdavaccess) {
var webdavserver = webdavaccess["webdavserver"] || "";
var webdavpath = webdavaccess["webdavpath"] || "/";
var webdavusername = webdavaccess["webdavusername"] || "";
var webdavpassword = webdavaccess["webdavpassword"] || "";
const client = createClient(
webdavserver,
{
username: webdavusername,
password: webdavpassword
}
)
client.getDirectoryContents(webdavpath).then((items) => {
var cloudpath = webdavpath+ '' + filename;
console.log("webdav saving to:", cloudpath);
fs.createReadStream(imagepath).pipe(client.createWriteStream(cloudpath));
callback();
}).catch((error) => {
callback("403");
console.log("Could not connect to webdav!")
});
} else {
callback("Error: no access data!")
}
}
var smallestScreenResolutions = {}; var smallestScreenResolutions = {};
io.on('connection', function (socket) { io.on('connection', function (socket) {
var whiteboardId = null; var whiteboardId = null;
@ -211,4 +285,9 @@ function getArgs() {
} }
}) })
return args return args
} }
process.on('unhandledRejection', error => {
// Will print "unhandledRejection err is not defined"
console.log('unhandledRejection', error.message);
})