Compare commits

...

19 commits
v0.1 ... master

Author SHA1 Message Date
slynn1324
d645768dd0
Delete .github/workflows/codeql-analysis.yml 2025-01-27 19:26:19 -06:00
slynn1324
6dc5f188e0
Merge pull request #7 from slynn1324/dependabot/npm_and_yarn/multi-6bc014718a
Bump path-to-regexp and express
2025-01-27 19:25:19 -06:00
dependabot[bot]
8af7f57094
Bump path-to-regexp and express
Bumps [path-to-regexp](https://github.com/pillarjs/path-to-regexp) to 0.1.12 and updates ancestor dependency [express](https://github.com/expressjs/express). These dependencies need to be updated together.


Updates `path-to-regexp` from 0.1.7 to 0.1.12
- [Release notes](https://github.com/pillarjs/path-to-regexp/releases)
- [Changelog](https://github.com/pillarjs/path-to-regexp/blob/master/History.md)
- [Commits](https://github.com/pillarjs/path-to-regexp/compare/v0.1.7...v0.1.12)

Updates `express` from 4.17.1 to 4.21.2
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/4.21.2/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.17.1...4.21.2)

---
updated-dependencies:
- dependency-name: path-to-regexp
  dependency-type: indirect
- dependency-name: express
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-28 01:19:24 +00:00
slynn1324
62a61aed72
Merge pull request #4 from ryanfiller/master
create firefox-extension, replace all chrome.func with browser.func
2025-01-27 10:35:56 -06:00
slynn1324
58c583d461
Merge pull request #3 from battlesloth/master
Add WebP support
2025-01-25 16:30:52 -06:00
ryanfiller
219f6d48b5 create firefox-extension, replace all chrome.func with browser.func 2024-05-16 13:07:00 -05:00
Jeff Rector
0cc19c48c0 Add WebP support 2023-02-01 07:38:20 -05:00
slynn1324
fed01f948b fix addpin 2021-10-04 20:17:12 -05:00
slynn1324
0bccaef904 let calls with x-api-key bypass csrf checking 2021-10-04 19:57:46 -05:00
slynn1324
0d1037a1f0 Merge branch 'master' of github.com:slynn1324/tinypin 2021-10-04 16:29:44 -05:00
slynn1324
7e815f6512 fix typo 2021-10-04 16:29:37 -05:00
slynn1324
59c07e8a43 updated build scripts 2021-10-04 16:16:56 -05:00
slynn1324
ea96135e0c Merge branch 'master' of github.com:slynn1324/tinypin 2021-10-04 15:47:50 -05:00
slynn1324
efa93f409f fix github scanned issues 2021-10-04 15:47:42 -05:00
slynn1324
38d8fa6eab
Create codeql-analysis.yml 2021-10-04 14:44:31 -05:00
slynn1324
a573f4a073 updated dependencies to resolve security issues 2021-10-04 14:43:12 -05:00
slynn1324
8b866d1a72 updated bulma-custom dependencies and regenerated style.css 2021-10-04 14:36:56 -05:00
slynn1324
0f3fc05594 Added file upload capabilties via the add pin dialog or drag-and-drop on the board page 2021-10-04 14:27:29 -05:00
slynn1324
8ee0acda17 update readme with paypal link 2021-09-27 15:42:02 -05:00
35 changed files with 12231 additions and 2429 deletions

View file

@ -4,8 +4,8 @@
#only include these files
!main.js
!server
!static
!client
!templates
!package.json
!package-lock.json
!LICENSE
!LICENSE

View file

@ -16,4 +16,4 @@ RUN apk add build-base python3 && npm install --verbose && apk del build-base py
RUN mkdir /data && npm install
ENTRYPOINT ["sh", "-c" , "node server.js -i /data/images -d /data/tinypin.db"]
ENTRYPOINT ["sh", "-c" , "node main.js -i /data/images -d /data/tinypin.db"]

View file

@ -108,3 +108,7 @@ There is trivial security on the web pages to allow for multiple user support.
- library > [node-fetch](https://www.npmjs.com/package/node-fetch)
- library > [sharp](https://www.npmjs.com/package/sharp)
- library > [yargs](https://www.npmjs.com/package/yargs)
## buy me a beer
If you find this useful and feel so inclinced, https://paypal.me/slynn1324. Otherwise, simply enjoy.

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
{
"devDependencies": {
"bulma": "^0.9.1",
"node-sass": "^5.0.0"
"bulma": "0.9.3",
"node-sass": "6.0.1"
}
}

File diff suppressed because it is too large Load diff

View file

@ -73,6 +73,23 @@ app.addSetter("hash.parse", (data) => {
});
app.addSetter("load.user", async (data) => {
store.do("loader.show");
let res = await fetch("/api/whoami");
if ( res.status == 200 ){
data.user = await res.json();
window.csrfToken = data.user.csrf;
} else {
console.log("error getting user");
}
store.do("loader.hide");
});
app.addSetter("load.boards", async (data) => {
store.do("loader.show");
@ -138,7 +155,7 @@ app.addSetter('addPinModal.save', async (data) => {
if ( boardId == "new" ){
let res = await fetch('api/boards', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
headers: { 'Content-Type': 'application/json', 'x-csrf-token': window.csrfToken },
body: JSON.stringify({
"name": data.addPinModal.newBoardName
})
@ -161,7 +178,8 @@ app.addSetter('addPinModal.save', async (data) => {
let res = await fetch('api/pins', {
method: 'POST',
headers: {
'Content-Type': "application/json"
'Content-Type': "application/json",
'x-csrf-token': window.csrfToken
},
body: JSON.stringify(postData)
});
@ -349,6 +367,8 @@ if ( target ){
store.do('hash.parse');
store.do("load.user");
store.do('load.boards');
Reef.databind(appComponent);

View file

@ -71,12 +71,15 @@ app.addSetter('load.board', async (data, force) => {
});
app.addSetter('load.user', async (data) => {
console.log("load.user");
store.do("loader.show");
let res = await fetch("/api/whoami");
data.user = await res.json();
window.uid = data.user.id;
window.csrfToken = data.user.csrf;
dispatchSocketConnect();
store.do("loader.hide");
@ -96,6 +99,107 @@ app.addSetter("hash.update", (data) => {
}
});
app.addSetter("app.uploadDroppedFiles", async (data, evt) => {
let boardId = store.data.board.id;
const supportedTypes = ["image/jpeg","image/png","image/webp"];
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 ( !supportedTypes.includes(file.type)){
window.alert("Unsupported file type. JPEG, PNG, and WebP 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,
headers: {
"x-csrf-token": window.csrfToken
}
});
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(){
window.dispatchEvent(new CustomEvent("socket-connect"));
}
@ -121,7 +225,8 @@ let store = new Reef.Store({
previewImageUrl: null,
siteUrl: "",
description: "",
saveInProgress: false
saveInProgress: false,
didYouKnowDragAndDropMessageDisabled: window.localStorage.addPinModal_didYouKnowDragAndDropMessageDisabled == "true" || false
},
pinZoomModal: {
active: false,
@ -167,6 +272,23 @@ const appComponent = new Reef("#app", {
<div id="editBoardModal"></div>
<div id="aboutModal"></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>
}
@ -290,4 +412,55 @@ 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);
}
}
};
function getCookie(name) {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop().split(';').shift();
}

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.is-active {
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.newBoardName = "";
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) => {
if ( data.addPinModal.imageUrl.startsWith("http") ){
( async() => {
let res = await fetch(data.addPinModal.imageUrl, {
@ -38,6 +47,7 @@ app.addSetter('addPinModal.updatePreview', (data) => {
} else {
data.addPinModal.previewImageUrl = null;
}
});
app.addSetter('addPinModal.save', async (data) => {
@ -48,37 +58,64 @@ app.addSetter('addPinModal.save', async (data) => {
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', {
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);
if ( data.addPinModal.uploadFile ){
// do file upload
console.log("attempting multipart file uploading");
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 ){
data.board.pins.push(newPin);
}
window.localStorage.addPinLastBoardId = boardId;
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;
store.do("addPinModal.close");
} else {
// if we don't have a listening socket, we need to trigger our own update
if ( boardId == "new" && !window.socketConnected ){
store.do("load.boards");
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', {
method: 'POST',
headers: {
'Content-Type': "application/json",
"x-csrf-token" : window.csrfToken
},
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");
@ -101,6 +138,56 @@ app.addGetter('addPinModal.isValid', (data) => {
return true;
});
app.addSetter('addPinModal.fileChosen', (data, target) => {
let file = target.files[0];
const supportedTypes = ["image/jpeg","image/png","image/webp"];
// check type
if ( !supportedTypes.includes(file.type)){
window.alert("Unsupported file type. JPEG, PNG and WebP 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", {
store: store,
@ -109,8 +196,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 options = "";
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 = '';
@ -134,6 +224,17 @@ app.addComponent('addPinModal', (store) => { return new Reef("#addPinModal", {
<button class="delete" aria-label="close" data-onclick="addPinModal.close"></button>
</header>
<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-left">
<img id="add-pin-modal-img" src="${data.addPinModal.previewImageUrl ? data.addPinModal.previewImageUrl : imagePlaceholder}" />
@ -141,6 +242,8 @@ app.addComponent('addPinModal', (store) => { return new Reef("#addPinModal", {
<div class="add-pin-flex-right">
<form>
<div class="field">
<label class="label">Board</label>
<div class="select">
@ -153,13 +256,31 @@ app.addComponent('addPinModal', (store) => { return new Reef("#addPinModal", {
${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">
<label class="label">Image Url</label>
<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 class="field">
<lable class="label">or choose file</lable>
<div class="control">
<input class="input" type="file" id="fileInput" />
</div>
</div>
`}
<div class="field">
<label class="label">Website Url</label>
<div class="control">

View file

@ -32,7 +32,10 @@ app.addSetter("brickwall.deletePin", async (data) => {
closeLightGallery();
let res = await fetch(`api/pins/${pinId}`, {
method: 'DELETE'
method: 'DELETE',
headers: {
'x-csrf-token': window.csrfToken
}
});
if ( res.status == 200 ){
@ -63,7 +66,7 @@ async function iosShare(){
let index = getLightGalleryIndex();
let pin = data.board.pins[index];
let result = await fetch("/api/pins/" + pin.id + "/otl", {method: 'POST'});
let result = await fetch("/api/pins/" + pin.id + "/otl", {method: 'POST', headers: {'x-csrf-token':window.csrfToken}});
let obj = await result.json();
let t = obj.t;

View file

@ -33,6 +33,7 @@ app.addSetter('editBoardModal.save', async (data) => {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-csrf-token': window.csrfToken
},
body: JSON.stringify({
name: name,
@ -70,7 +71,10 @@ app.addSetter('editBoardModal.delete', async (data) => {
let res = await fetch(`/api/boards/${boardId}`, {
method: 'DELETE'
method: 'DELETE',
headers: {
'x-csrf-token':window.csrfToken
}
});
if ( res.status == 200 ){

View file

@ -30,7 +30,7 @@ app.addGetter('editPinModal.isValid', (data) => {
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.boardId == data.editPinModal.pin.boardId ){
return false;
@ -56,7 +56,10 @@ app.addSetter("editPinModal.save", async (data) => {
// TODO: make a helper method
let res = await fetch("/api/boards", {
method: 'POST',
headers: {'Content-Type': 'application/json' },
headers: {
'Content-Type': 'application/json',
'x-csrf-token': window.csrfToken
},
body: JSON.stringify({
"name": data.editPinModal.newBoardName
})
@ -79,7 +82,8 @@ app.addSetter("editPinModal.save", async (data) => {
let res = await fetch('api/pins/' + pin.id, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
'Content-Type': 'application/json',
'x-csrf-token': window.csrfToken
},
body: JSON.stringify(postData)
});

View file

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

File diff suppressed because it is too large Load diff

View file

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

View file

@ -58,7 +58,11 @@ function getPinIndexById(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)); }

View file

@ -1,7 +1,9 @@
#!/bin/sh
VERSION=$(git rev-parse --verify --short HEAD)
docker build \
-t slynn1324/tinypin \
--build-arg VERSION=$(git rev-parse --verify --short HEAD) \
--build-arg VERSION=$VERSION \
.

View file

@ -0,0 +1,80 @@
/**
* Returns a handler which will open a new window when activated.
*/
function getClickHandler() {
return function(info, tab) {
if ( !info.srcUrl.startsWith('http') ){
window.alert("Image source is not a URL.");
return;
}
var w = 700;
var h = 800;
var left = (screen.width/2)-(w/2);
var top = (screen.height/2)-(h/2);
let s = "";
if ( info.linkUrl ){
s = info.linkUrl;
// strip the google images redirect
if ( s.startsWith("https://www.google.com/url?") ){
let parts = s.split("?");
if ( parts.length == 2 ){
let params = parts[1].split("&");
for( let i = 0; i < params.length; ++i ){
let kv = params[i].split("=");
if ( kv.length == 2 ){
if ( kv[0] == "url" ){
s = decodeURIComponent(kv[1]);
}
}
}
}
}
s = encodeURIComponent(s);
} else {
s = encodeURIComponent(info.pageUrl);
}
var q = "i=" + encodeURIComponent(info.srcUrl) + "&s=" + s;
browser.storage.sync.get({
server: 'http://localhost:3000'
}, function(items){
let server = items.server;
if ( !server.endsWith('/') ){
server += '/';
}
var url = server + 'addpin.html#' + q;
// Create a new window to the info page.
// browser.windows.create({ url: url, width: 520, height: 660 });
browser.windows.create({ url: url, width: w, height: h, left: left, top: top, type: 'popup' });
});
};
};
/**
* Create a context menu which will only show up for images.
*/
browser.contextMenus.create({
"title" : "add to tinypin",
"type" : "normal",
"contexts" : ["image"],
"onclick" : getClickHandler()
});

File diff suppressed because it is too large Load diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 224 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 329 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 536 B

View file

@ -0,0 +1,36 @@
{
"name": "add to tinypin",
"version": "1.0.0",
"description": "add to tinypin context menu plugin",
"manifest_version": 2,
"background" : {
"scripts": ["background.js"],
"persistent": true
},
"options_ui" : {
"page": "options.html",
"open_in_tab": false
},
"permissions" : [
"contextMenus",
"storage"
],
"icons": {
"16" : "icon16.png",
"32" : "icon32.png",
"48" : "icon48.png",
"128" : "icon128.png"
},
"browser_action" :{
"default_title": "add to tinypin",
"default_icon" :{
"16": "icon16.png",
"32": "icon32.png"
}
},
"browser_specific_settings": {
"gecko": {
"id": "@slynn1324"
}
}
}

View file

@ -0,0 +1,32 @@
<!DOCTYPE html>
<html>
<head>
<title>tinypin options</title>
<link rel="stylesheet" href="./bulma-custom.css" />
</head>
<body>
<div class="secton">
<div class="content" style="margin: 10px;">
<div class="field">
<label class="label">tinypin server url</label>
<div class="control">
<input class="input" id="server" type="text">
</div>
</div>
<button class="button is-success" id="save">Save</button>
<span id="status" style="line-height: 2.5em; color: #3273dc;"></span>
</div>
</div>
<script src="options.js"></script>
</body>
</html>

View file

@ -0,0 +1,25 @@
function restoreOptions(){
browser.storage.sync.get({
server: 'http://localhost:3000'
}, function(items){
document.getElementById('server').value = items.server;
});
}
function saveOptions(){
let server = document.getElementById('server').value;
browser.storage.sync.set({
server: server
}, function(){
let status = document.getElementById('status');
status.innerText = 'Options saved.';
setTimeout(function(){
status.innerText = '';
}, 1000);
});
}
document.addEventListener('DOMContentLoaded', restoreOptions);
document.getElementById('save').addEventListener('click', saveOptions);

2529
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -9,14 +9,18 @@
"author": "slynn1324",
"license": "ISC",
"dependencies": {
"better-sqlite3": "^7.1.2",
"better-sqlite3": "^7.4.3",
"body-parser": "^1.19.0",
"cookie-parser": "^1.4.5",
"eta": "^1.12.1",
"express": "^4.17.1",
"express-ws": "^4.0.0",
"csurf": "^1.11.0",
"eta": "^1.12.3",
"express": "^4.21.2",
"express-rate-limit": "^5.4.0",
"express-ws": "^5.0.2",
"multer": "^1.4.3",
"node-fetch": "^2.6.1",
"sharp": "^0.27.0",
"yargs": "^16.2.0"
"sanitize-filename": "^1.6.3",
"sharp": "^0.29.1",
"yargs": "^17.2.1"
}
}

View file

@ -8,6 +8,36 @@ function sendAuthCookie(res, c){
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) => {
// we will also accept the auth token in the x-api-key header
@ -60,9 +90,16 @@ module.exports = async (req, res, next) => {
next();
return;
} if ( req.method == "GET" && req.originalUrl == "/login" ){
if ( maybeGetUser(req) ){
res.redirect("./");
return;
}
console.log("login");
// res.type("html").sendFile(path.resolve('./templates/login.html'));
res.render("login", { registerEnabled: dao.getProperty("registerEnabled") });
res.render("login", { registerEnabled: dao.getProperty("registerEnabled"), csrfToken: req.csrfToken() });
return;
} else if ( req.method == "POST" && req.originalUrl == "/login" ){
let username = req.body.username;
@ -99,7 +136,7 @@ module.exports = async (req, res, next) => {
return;
}
res.render("register", {});
res.render("register", { csrfToken: req.csrfToken() });
return;
} else if ( req.method == "POST" && req.originalUrl == "/register" ){
@ -135,26 +172,27 @@ module.exports = async (req, res, next) => {
return;
}
// if we made it this far, we're eady to check for the cookie
let s = req.cookies.s;
// // 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 ){
req.user = {
id: s.i,
name: s.u
}
}
} catch (err) {
console.error(`error parsing cookie: `, err);
}
}
// // 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 ){
// req.user = {
// id: s.i,
// name: s.u
// }
// }
// } catch (err) {
// console.error(`error parsing cookie: `, err);
// }
// }
req.user = maybeGetUser(req);
if ( !req.user ){
res.redirect("/login");

View file

@ -1,6 +1,8 @@
const yargs = require('yargs');
const express = require('express');
const bodyParser = require('body-parser');
const multer = require("multer")
const RateLimit = require("express-rate-limit");
const path = require('path');
const cookieParser = require('cookie-parser');
const tokenUtil = require('./token-utils.js');
@ -9,6 +11,20 @@ const conf = require("./conf.js");
const imageUtils = require('./image-utils.js');
var eta = require("eta");
const tokenUtils = require('./token-utils.js');
const csrf = require("csurf");
const sanitizeFilename = require("sanitize-filename");
// 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
// enable a rate limit so we can't swamp the server/filesystem -- 100 per second here. Pretty fast because we're likely
// running locally and want to load images fast.
// GitHub CodeQL - js/missing-rate-limiting
const rateLimiter = new RateLimit({
windowMs: 1000,
max: 100
});
module.exports = async () => {
@ -84,13 +100,26 @@ module.exports = async () => {
app.set("views", "./templates")
const expressWs = require('express-ws')(app);
app.use(rateLimiter); // rate limiting
app.use(bodyParser.raw({type: 'image/jpeg', limit: '25mb'})); // accept image/jpeg files only
app.use(bodyParser.urlencoded({ extended: false }))
app.use(bodyParser.json());
app.set('json spaces', 2);
app.use(cookieParser());
// only appy csrf if we don't have an x-api-key header. The value of the x-api-key will be validated by the auth middleware
app.use( (req,res,next) => {
let apiKey = req.headers["x-api-key"];
if ( apiKey ){
next();
} else {
csrf({cookie:true})(req,res,next);
}
});
// // all other endpoints require csrf
// app.use(csrf({cookie:true}));
// accept websocket connections. currently are parsing the userid from the path to
// map the connections to only notify on changes from the same user.
@ -141,7 +170,7 @@ module.exports = async () => {
res.sendStatus(403);
return;
}
res.send({name: user.username, id: user.id, admin: user.admin, version: VERSION});
res.send({name: user.username, id: user.id, admin: user.admin, version: VERSION, csrf: req.csrfToken()});
});
// list boards
@ -223,7 +252,7 @@ module.exports = async () => {
res.status(404).send(NOT_FOUND);
}
} catch (err) {
console.log(`Error deleting board#${req.params.boardId}:`, err);
console.log('Error deleting board# %s', req.params.boardId, err);
res.status(500).send(SERVER_ERROR);
}
});
@ -238,7 +267,7 @@ module.exports = async () => {
res.status(404).send(NOT_FOUND);
}
} catch (err){
console.error(`Error getting pin#${req.params.pinId}: ${err.message}`, err);
console.error('Error getting pin# %s', req.params.pinId, err);
res.status(500).send(SERVER_ERROR);
}
});
@ -291,7 +320,7 @@ module.exports = async () => {
res.status(404).send(NOT_FOUND);
}
} catch (err) {
console.log(`Error updating pin#${req.params.pinId}`, err);
console.log('Error updating pin# %s', req.params.pinId, err);
res.status(500).send(SERVER_ERROR);
}
@ -321,7 +350,7 @@ module.exports = async () => {
}
} catch (err){
console.log(`Error deleting pin#${req.params.pinId}`, err);
console.log('Error deleting pin# %s', req.params.pinId, err);
res.status(500).send(SERVER_ERROR);
}
});
@ -340,10 +369,12 @@ module.exports = async () => {
res.status(200).send({t: token});
});
// api method that are not subject to CSRF checks
// handle raw uploads for pin creation
app.post("/up", async (req, res) => {
try {
require("fs").writeFileSync("up.jpg", req.body);
// try to parse the image first... if this blows up we'll stop early
@ -358,7 +389,7 @@ module.exports = async () => {
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);
@ -371,6 +402,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) => {
let s = req.cookies['s'];
console.log("s=" + s);
@ -394,6 +457,7 @@ module.exports = async () => {
}
res.render("settings", {
csrfToken: req.csrfToken(),
registerEnabled: registerEnabled,
users: users,
userId: req.user.id
@ -441,8 +505,6 @@ module.exports = async () => {
let password = req.body.password;
let repeatPassword = req.body.repeatPassword;
console.log(`username: ${username} password: ${password} rp: ${repeatPassword}`);
if ( password != repeatPassword ){
res.redirect("./settings#password-match")
return;
@ -454,7 +516,7 @@ module.exports = async () => {
try{
dao.createUser(username, 0, key, salt);
} catch (err){
console.log("error creating user " + username, err);
console.log("error creating user %s", username, err);
res.redirect("./settings#create-user-error");
return;
}
@ -466,11 +528,17 @@ module.exports = async () => {
let uid = req.body.uid;
// uids must ONLY be integer numbers, to ensure that this is safe to use in the path below
if ( !uid.match(/^[0-9]+$/) ){
console.log("Invalid uid: %s", uid);
res.redirect("./settings#delete-user-error");
}
try {
dao.deleteUser(uid);
require("fs").rmdirSync(conf.getImagePath() + "/" + uid , { recursive: true });
} catch (err){
console.log("error deleting user " + uid, err);
console.log("error deleting user %s", uid, err);
res.redirect("./settings#delete-user-error");
return;
}

View file

@ -26,6 +26,7 @@
<p class="modal-card-title">tinypin &raquo; log in</p>
</header>
<form method="post" action="./login">
<input type="hidden" name="_csrf" value="<%= it.csrfToken %>" />
<section class="modal-card-body">
<div class="field">

View file

@ -26,6 +26,7 @@
<p class="modal-card-title">tinypin &raquo; create account</p>
</header>
<form method="post" action="./register">
<input type="hidden" name="_csrf" value="<%= it.csrfToken %>" />
<section class="modal-card-body">
<div class="field">

View file

@ -69,6 +69,7 @@
<h1 style="border-bottom: 1px solid #eee;"><strong>Settings</strong></h1>
<br />
<form method="POST" action="./settings">
<input type="hidden" name="_csrf" value="<%= it.csrfToken %>" />
<input type="hidden" name="action" value="updateSettings">
<div class="field is-horizontal">
<div class="field-label is-normal">
@ -101,7 +102,8 @@
<br />
<form method="POST" action="./settings">
<input type="hidden" name="action" value="updateUsers">
<input type="hidden" name="_csrf" value="<%= it.csrfToken %>" />
<input type="hidden" name="action" value="updateUsers" />
<table class="table" style="width: 100%;">
<thead>
<tr>
@ -165,6 +167,7 @@
<p class="modal-card-title">tinypin &raquo; create account</p>
</header>
<form method="post" action="./settings" onsubmit="return submitCreateUserForm()">
<input type="hidden" name="_csrf" value="<%= it.csrfToken %>" />
<input type="hidden" name="action" value="createUser" />
<section class="modal-card-body">
@ -202,6 +205,7 @@
</div>
<form id="deleteUserForm" action="./settings" method="POST">
<input type="hidden" name="_csrf" value="<%= it.csrfToken %>" />
<input type="hidden" name="action" value="deleteUser" />
<input type="hidden" id="deleteUserUid" name="uid" value="" />
</form>