start auth

This commit is contained in:
slynn1324 2021-01-23 18:05:39 -06:00
parent 3cee573bdb
commit 446cfe17c6
16 changed files with 391 additions and 24 deletions

22
package-lock.json generated
View file

@ -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",

View file

@ -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",

112
public/create-account.html Normal file
View file

@ -0,0 +1,112 @@
<!DOCTYPE html>
<html>
<head>
<title>tinypin</title>
<meta charset="utf-8">
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
<link rel="apple-touch-icon" sizes="180x180" href="icons/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="icons/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="icons/favicon-16x16.png">
<link rel="manifest" href="icons/site.webmanifest">
<link rel="mask-icon" href="icons/safari-pinned-tab.svg" color="#5bbad5">
<link rel="shortcut icon" href="icons/favicon.ico">
<meta name="msapplication-TileColor" content="#da532c">
<meta name="msapplication-config" content="icons/browserconfig.xml">
<meta name="theme-color" content="#ffffff">
<link rel="stylesheet" href="bulma-custom.css" />
</head>
<body>
<div class="modal is-active">
<div class="modal-background" data-onclick="aboutModal.close"></div>
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title">tinypin &raquo; create account</p>
</header>
<form method="post" action="http://localhost:3000/create-account">
<section class="modal-card-body">
<div class="field">
<label class="label" for="username">username</label>
<div class="control">
<input class="input" name="username" id="username" type="text">
</div>
</div>
<div class="field">
<label class="label" for="password">password</label>
<div class="control">
<input class="input" name="password" id="password" type="password">
</div>
</div>
</section>
<footer class="modal-card-foot">
<input type="submit">send</input>
<button id="submitButton" class="button is-success" disabled type="submit">create account</button>
<a class="button" href="login.html">login</a>
</footer>
</form>
</div>
</div>
<script>
const username = document.getElementById("username");
const password = document.getElementById("password");
const submitButton = document.getElementById("submitButton");
const validate = () => {
let valid = true;
if ( username.value.length < 1 ){
if ( username.getAttribute("data-visited") == "y" ){
username.classList.add("is-danger");
}
valid = false;
} else {
username.classList.remove("is-danger");
}
if ( password.value.length < 1 ){
if ( password.getAttribute("data-visited") == "y" ){
password.classList.add("is-danger");
}
valid = false;
} else {
password.classList.remove('is-danger');
}
if ( valid ){
submitButton.disabled = false;
} else {
submitButton.disabled = true;
}
}
// document.addEventListener('input', validate);
// document.addEventListener('focusin', (evt) => {
// if ( evt.target == username ){
// username.setAttribute("data-visited", "y");
// } else if ( evt.target == password ){
// password.setAttribute("data-visited", "y");
// }
// });
// document.addEventListener('focusout', (evt) => {
// validate();
// });
</script>
</body>
</html>

View file

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 537 B

After

Width:  |  Height:  |  Size: 537 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 711 B

After

Width:  |  Height:  |  Size: 711 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Before After
Before After

113
public/login.html Normal file
View file

@ -0,0 +1,113 @@
<!DOCTYPE html>
<html>
<head>
<title>tinypin</title>
<meta charset="utf-8">
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
<link rel="apple-touch-icon" sizes="180x180" href="icons/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="icons/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="icons/favicon-16x16.png">
<link rel="manifest" href="icons/site.webmanifest">
<link rel="mask-icon" href="icons/safari-pinned-tab.svg" color="#5bbad5">
<link rel="shortcut icon" href="icons/favicon.ico">
<meta name="msapplication-TileColor" content="#da532c">
<meta name="msapplication-config" content="icons/browserconfig.xml">
<meta name="theme-color" content="#ffffff">
<link rel="stylesheet" href="bulma-custom.css" />
</head>
<body>
<div class="modal is-active">
<div class="modal-background" data-onclick="aboutModal.close"></div>
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title">tinypin &raquo; login</p>
</header>
<form method="post" action="http://localhost:3000/login">
<section class="modal-card-body">
<div class="field">
<label class="label" for="username">username</label>
<div class="control">
<input class="input" name="username" id="username" type="text">
</div>
</div>
<div class="field">
<label class="label" for="password">password</label>
<div class="control">
<input class="input" name="password" id="password" type="password">
</div>
</div>
</section>
<footer class="modal-card-foot">
<button id="submitButton" class="button is-success" type="submit">login</button>
<a class="button" href="create-account.html">create account</a>
<span id="nope"></span>
</footer>
</form>
</div>
</div>
<script>
if ( window.location.hash == "#nope" ){
document.getElementById("nope").innerText = "nope.";
}
const username = document.getElementById("username");
const password = document.getElementById("password");
const submitButton = document.getElementById("submitButton");
const validate = () => {
let valid = true;
if ( username.value.length < 1 ){
if ( username.getAttribute("data-visited") == "y" ){
username.classList.add("is-danger");
}
valid = false;
} else {
username.classList.remove("is-danger");
}
if ( password.value.length < 1 ){
if ( password.getAttribute("data-visited") == "y" ){
password.classList.add("is-danger");
}
valid = false;
} else {
password.classList.remove('is-danger');
}
if ( valid ){
submitButton.disabled = false;
} else {
submitButton.disabled = true;
}
}
// document.addEventListener('input', validate);
// document.addEventListener('focusin', (evt) => {
// if ( evt.target == username ){
// username.setAttribute("data-visited", "y");
// } else if ( evt.target == password ){
// password.setAttribute("data-visited", "y");
// }
// });
// document.addEventListener('focusout', (evt) => {
// validate();
// });
</script>
</body>
</html>

167
server.js
View file

@ -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){