diff --git a/package-lock.json b/package-lock.json
index 5c85b2a..f47a0eb 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10,6 +10,7 @@
"dependencies": {
"better-sqlite3": "^7.1.2",
"body-parser": "^1.19.0",
+ "cookie-parser": "^1.4.5",
"express": "^4.17.1",
"node-fetch": "^2.6.1",
"sharp": "^0.27.0",
@@ -452,6 +453,18 @@
"node": ">= 0.6"
}
},
+ "node_modules/cookie-parser": {
+ "version": "1.4.5",
+ "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.5.tgz",
+ "integrity": "sha512-f13bPUj/gG/5mDr+xLmSxxDsB9DQiTIfhJS/sqjrmfAWiAN+x2O4i/XguTL9yDZ+/IFDanJ+5x7hC4CXT9Tdzw==",
+ "dependencies": {
+ "cookie": "0.4.0",
+ "cookie-signature": "1.0.6"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
"node_modules/cookie-signature": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
@@ -1920,6 +1933,15 @@
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz",
"integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg=="
},
+ "cookie-parser": {
+ "version": "1.4.5",
+ "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.5.tgz",
+ "integrity": "sha512-f13bPUj/gG/5mDr+xLmSxxDsB9DQiTIfhJS/sqjrmfAWiAN+x2O4i/XguTL9yDZ+/IFDanJ+5x7hC4CXT9Tdzw==",
+ "requires": {
+ "cookie": "0.4.0",
+ "cookie-signature": "1.0.6"
+ }
+ },
"cookie-signature": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
diff --git a/package.json b/package.json
index d958e46..7d07d18 100644
--- a/package.json
+++ b/package.json
@@ -11,6 +11,7 @@
"dependencies": {
"better-sqlite3": "^7.1.2",
"body-parser": "^1.19.0",
+ "cookie-parser": "^1.4.5",
"express": "^4.17.1",
"node-fetch": "^2.6.1",
"sharp": "^0.27.0",
diff --git a/static/bulma-custom.css b/public/bulma-custom.css
similarity index 100%
rename from static/bulma-custom.css
rename to public/bulma-custom.css
diff --git a/public/create-account.html b/public/create-account.html
new file mode 100644
index 0000000..0dc8d9e
--- /dev/null
+++ b/public/create-account.html
@@ -0,0 +1,112 @@
+
+
+
+ tinypin
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ tinypin » create account
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/static/icons/android-chrome-192x192.png b/public/icons/android-chrome-192x192.png
similarity index 100%
rename from static/icons/android-chrome-192x192.png
rename to public/icons/android-chrome-192x192.png
diff --git a/static/icons/android-chrome-512x512.png b/public/icons/android-chrome-512x512.png
similarity index 100%
rename from static/icons/android-chrome-512x512.png
rename to public/icons/android-chrome-512x512.png
diff --git a/static/icons/apple-touch-icon.png b/public/icons/apple-touch-icon.png
similarity index 100%
rename from static/icons/apple-touch-icon.png
rename to public/icons/apple-touch-icon.png
diff --git a/static/icons/browserconfig.xml b/public/icons/browserconfig.xml
similarity index 100%
rename from static/icons/browserconfig.xml
rename to public/icons/browserconfig.xml
diff --git a/static/icons/favicon-16x16.png b/public/icons/favicon-16x16.png
similarity index 100%
rename from static/icons/favicon-16x16.png
rename to public/icons/favicon-16x16.png
diff --git a/static/icons/favicon-32x32.png b/public/icons/favicon-32x32.png
similarity index 100%
rename from static/icons/favicon-32x32.png
rename to public/icons/favicon-32x32.png
diff --git a/static/icons/favicon.ico b/public/icons/favicon.ico
similarity index 100%
rename from static/icons/favicon.ico
rename to public/icons/favicon.ico
diff --git a/static/icons/mstile-150x150.png b/public/icons/mstile-150x150.png
similarity index 100%
rename from static/icons/mstile-150x150.png
rename to public/icons/mstile-150x150.png
diff --git a/static/icons/safari-pinned-tab.svg b/public/icons/safari-pinned-tab.svg
similarity index 100%
rename from static/icons/safari-pinned-tab.svg
rename to public/icons/safari-pinned-tab.svg
diff --git a/static/icons/site.webmanifest b/public/icons/site.webmanifest
similarity index 100%
rename from static/icons/site.webmanifest
rename to public/icons/site.webmanifest
diff --git a/public/login.html b/public/login.html
new file mode 100644
index 0000000..f15977c
--- /dev/null
+++ b/public/login.html
@@ -0,0 +1,113 @@
+
+
+
+ tinypin
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/server.js b/server.js
index 81cccb6..278eb65 100644
--- a/server.js
+++ b/server.js
@@ -9,6 +9,9 @@ const sharp = require('sharp');
const fs = require('fs').promises;
const path = require('path');
const fetch = require('node-fetch');
+const crypto = require('crypto');
+const cookieParser = require('cookie-parser');
+const { send } = require('process');
process.on('SIGINT', () => {
console.info('ctrl+c detected, exiting tinypin');
@@ -71,11 +74,64 @@ console.log('');
const db = betterSqlite3(DB_PATH);
// express config
const app = express();
-app.use(express.static('static'));
-app.use(express.static(IMAGE_PATH));
+app.use(express.static('public'));
+
app.use(bodyParser.urlencoded({ extended: false }))
app.use(bodyParser.json());
app.set('json spaces', 2);
+app.use(cookieParser());
+
+
+app.post("/login", (req, res) => {
+
+ let username = req.body.username;
+ let passhash = hashPassword(req.body.password);
+
+ let result = db.prepare("SELECT * FROM users WHERE username = @username AND passhash = @passhash").get({username: username, passhash: passhash});
+
+ if ( result ){
+ console.log(`login ok user ${username}`);
+
+ res.cookie('s', JSON.stringify({
+ i: result.id,
+ u: result.username,
+ d: new Date().toISOString()
+ }));
+
+ res.redirect("./");
+
+ } else {
+ console.log(`login failed for user ${username}`);
+ res.redirect("/login.html#nope");
+ }
+
+});
+
+// auth -- if the cookie is set exctract the user info, otherwise redirect to /login.html
+app.use( (req, res, next) => {
+
+ // todo - allow basic auth for apis?
+ let s = req.cookies.s;
+
+ if ( s ){
+ s = JSON.parse(s);
+ req.user = {
+ id: s.i,
+ name: s.u
+ }
+
+ next();
+
+ } else {
+ console.log("not logged in");
+ res.redirect("/login.html"); // this means we have issues with a context path, but is needed for image redirects to work
+ }
+
+});
+
+
+app.use(express.static('static'));
+app.use(express.static(IMAGE_PATH));
//emulate slow down
if ( SLOW ){
@@ -93,6 +149,7 @@ const ALREADY_EXISTS = {status: "error", error: "already exists"};
const SERVER_ERROR = {status: "error", error: "server error"};
initDb();
+const passwordSalt = getPasswordSalt();
// list boards
app.get("/api/boards", async (req, res) => {
@@ -100,7 +157,7 @@ app.get("/api/boards", async (req, res) => {
let boards = db.prepare("SELECT * FROM boards").all();
for( let i = 0; i < boards.length; ++i ){
- let result = db.prepare("SELECT id FROM pins WHERE boardId = ? order by createDate limit 1").get(boards[i].id);
+ let result = db.prepare("SELECT id FROM pins WHERE userId = @userId and boardId = @boardId order by createDate limit 1").get({userId:req.user.id, boardId:boards[i].id});
if ( result ) {
boards[i].titlePinId = result.id;
} else {
@@ -119,10 +176,10 @@ app.get("/api/boards", async (req, res) => {
app.get("/api/boards/:boardId", async (req, res) => {
try{
- let board = db.prepare("SELECT * FROM boards WHERE id = ?").get(req.params.boardId);
+ let board = db.prepare("SELECT * FROM boards WHERE userId = @userId and id = @boardId").get({userId:req.user.id, boardId:req.params.boardId});
if ( board ){
- board.pins = db.prepare("SELECT * FROM pins WHERE boardId = ?").all(req.params.boardId);
+ board.pins = db.prepare("SELECT * FROM pins WHERE userId = @userId and boardId = @boardId").all({userId:req.user.id, boardId:req.params.boardId});
res.send(board);
} else {
@@ -137,9 +194,9 @@ app.get("/api/boards/:boardId", async (req, res) => {
// create board
app.post('/api/boards', (req, res) => {
try{
- let result = db.prepare("INSERT INTO boards (name, createDate) VALUES (@name, @createDate)").run({name: req.body.name, createDate: new Date().toISOString()});
+ let result = db.prepare("INSERT INTO boards (name, userId, createDate) VALUES (@name, @userId, @createDate)").run({name: req.body.name, userId: req.user.id, createDate: new Date().toISOString()});
let id = result.lastInsertRowid;
- let board = db.prepare("SELECT * FROM boards WHERE id = ?").get(id);
+ let board = db.prepare("SELECT * FROM boards WHERE userId = @userId and id = @boardId").get({userId: req.user.id, boardId: id});
board.titlePinId = 0;
res.send(board);
console.log(`Created board#${id} ${req.body.name}`);
@@ -157,7 +214,7 @@ app.post('/api/boards', (req, res) => {
// update board
app.post("/api/boards/:boardId", (req, res) =>{
try{
- let result = db.prepare("UPDATE boards SET name = @name WHERE id = @boardId").run({name: req.body.name, boardId: req.params.boardId});
+ let result = db.prepare("UPDATE boards SET name = @name WHERE userId = @userId and id = @boardId").run({name: req.body.name, userId: req.user.id, boardId: req.params.boardId});
if ( result.changes == 1 ){
res.send(OK);
} else {
@@ -173,14 +230,14 @@ app.post("/api/boards/:boardId", (req, res) =>{
app.delete("/api/boards/:boardId", async (req, res) => {
try{
- let pins = db.prepare("SELECT id FROM pins WHERE boardId = ?").all(req.params.boardId);
+ let pins = db.prepare("SELECT id FROM pins WHERE userId = @userId and boardId = @boardId").all({userId:req.user.id, boardId:req.params.boardId});
for ( let i = 0; i < pins.length; ++i ){
await fs.unlink(getThumbnailImagePath(pins[i].id).file);
await fs.unlink(getOriginalImagePath(pins[i].id).file);
}
- let result = db.prepare("DELETE FROM pins WHERE boardId = ?").run(req.params.boardId);
- result = db.prepare("DELETE FROM boards WHERE id = ?").run(req.params.boardId);
+ let result = db.prepare("DELETE FROM pins WHERE userId = @userId and boardId = @boardId").run({userId:req.user.id, boardId:req.params.boardId});
+ result = db.prepare("DELETE FROM boards WHERE userId = @userId and id = @boardId").run({userId: req.user.id, boardId:req.params.boardId});
if ( result.changes == 1 ){
res.send(OK);
@@ -196,7 +253,7 @@ app.delete("/api/boards/:boardId", async (req, res) => {
// get pin
app.get("/api/pins/:pinId", (req, res) => {
try {
- let pin = db.prepare('SELECT * FROM pins WHERE id = ?').get(req.params.pinId);
+ let pin = db.prepare('SELECT * FROM pins WHERE userId = @userId and id = @pinId').get({userId: req.user.id, pinId:req.params.pinId});
if ( pin ){
res.send(pin);
} else {
@@ -224,6 +281,7 @@ app.post("/api/pins", async (req, res) => {
originalWidth,
thumbnailHeight,
thumbnailWidth,
+ userId,
createDate
) VALUES (
@boardId,
@@ -235,6 +293,7 @@ app.post("/api/pins", async (req, res) => {
@originalWidth,
@thumbnailHeight,
@thumbnailWidth,
+ @userId,
@createDate)
`).run({
boardId: req.body.boardId,
@@ -246,6 +305,7 @@ app.post("/api/pins", async (req, res) => {
originalWidth: image.original.width,
thumbnailHeight: image.thumbnail.height,
thumbnailWidth: image.thumbnail.width,
+ userId: req.user.id,
createDate: new Date().toISOString()
});
@@ -262,7 +322,7 @@ app.post("/api/pins", async (req, res) => {
console.log(`Saved thumbnail to: ${thumbnailImagePath.file}`);
// return the newly created row
- let pin = db.prepare("SELECT * FROM pins WHERE id = ?").get(id);
+ let pin = db.prepare("SELECT * FROM pins WHERE userId = @userId and id = @pinId").get({userId: req.user.id, pinId: id});
res.send(pin);
} catch (err) {
@@ -279,8 +339,9 @@ app.post("/api/pins/:pinId", (req,res) => {
siteUrl = @siteUrl,
description = @description,
sortOrder = @sortOrder
- WHERE id = @pinId
+ WHERE userId = @userId and id = @pinId
`).run({
+ userId: req.user.id,
pinId: req.params.pinId,
boardId: req.body.boardId,
siteUrl: req.body.siteUrl,
@@ -304,12 +365,12 @@ app.post("/api/pins/:pinId", (req,res) => {
app.delete("/api/pins/:pinId", async (req, res) => {
try {
- await fs.unlink(getThumbnailImagePath(req.params.pinId).file);
- await fs.unlink(getOriginalImagePath(req.params.pinId).file);
-
- let result = db.prepare('DELETE FROM pins WHERE id = ?').run(req.params.pinId);
+ let result = db.prepare('DELETE FROM pins WHERE userId = @userId and id = @pinId').run({userId: req.user.id, pinId:req.params.pinId});
if ( result.changes == 1 ){
+ await fs.unlink(getThumbnailImagePath(req.params.pinId).file);
+ await fs.unlink(getOriginalImagePath(req.params.pinId).file);
+
console.log(`deleted pin#${req.params.pinId}`);
res.send(OK);
} else {
@@ -322,6 +383,38 @@ app.delete("/api/pins/:pinId", async (req, res) => {
});
+
+app.post("/create-account", (req, res) => {
+
+ console.log(`creating user '${req.body.username}'`);
+
+ let passhash = hashPassword(req.body.password);
+
+ let result = db.prepare('INSERT INTO users (username, passhash) VALUES (@username, @passhash)').run({username: req.body.username, passhash: passhash});
+
+ console.log(` user pk = ${result.lastInsertRowid}`);
+
+ let c = {
+ i: result.lastInsertRowid,
+ u: req.body.username,
+ d: new Date().toISOString()
+ }
+
+ res.cookie('s', JSON.stringify(c));
+
+ res.redirect("create-account.html");
+
+});
+
+app.get("/whoami", (req, res) => {
+ res.send(req.user);
+});
+
+function hashPassword(pw){
+ return crypto.createHash('sha256', passwordSalt).update(pw).digest('hex');
+}
+
+
// start listening
app.listen(PORT, () => {
console.log(`tinypin is running at http://localhost:${PORT}`);
@@ -346,14 +439,34 @@ function initDb(){
console.log(" running migration v1");
db.prepare(`
- CREATE TABLE IF NOT EXISTS boards (
- id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
- name TEXT NOT NULL UNIQUE,
- createDate TEXT)
+ CREATE TABLE users (
+ id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ username TEXT NOT NULL UNIQUE,
+ passhash TEXT NOT NULL,
+ createDate TEXT
+ )
`).run();
db.prepare(`
- CREATE TABLE IF NOT EXISTS pins (
+ CREATE TABLE properties (
+ key TEXT NOT NULL PRIMARY KEY,
+ value TEXT NOT NULL
+ )
+ `).run();
+
+ db.prepare(`
+ CREATE TABLE boards (
+ id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ name TEXT NOT NULL UNIQUE,
+ userId INTEGER NOT NULL,
+ createDate TEXT,
+
+ FOREIGN KEY (userId) REFERENCES users(id)
+ )
+ `).run();
+
+ db.prepare(`
+ CREATE TABLE pins (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
boardId INTEGER NOT NULL,
imageUrl TEXT,
@@ -364,12 +477,15 @@ function initDb(){
originalWidth INTEGER,
thumbnailHeight INTEGER,
thumbnailWidth INTEGER,
+ userId INTEGER NOT NULL,
createDate TEXT,
- FOREIGN KEY (boardId) REFERENCES boards(id)
+ FOREIGN KEY (boardId) REFERENCES boards(id),
+ FOREIGN KEY (userId) REFERENCES users(id)
)
`).run();
+ db.prepare("INSERT INTO properties (key, value) VALUES (@key, @value)").run({key: 'pwsalt', value: crypto.randomBytes(32).toString('hex')});
db.prepare("INSERT INTO migrations (id, createDate) VALUES ( @id, @createDate )").run({id:1, createDate: new Date().toISOString()});
schemaVersion = 1;
@@ -377,7 +493,10 @@ function initDb(){
console.log(`database ready - schema version v${schemaVersion}`);
console.log('');
+}
+function getPasswordSalt(){
+ return db.prepare('SELECT value FROM properties WHERE key = ?').get('pwsalt').value;
}
async function downloadImage(imageUrl){