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 + + + + + + + + + + + + + + + + + + + + + + \ 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){