You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
pastebin/node/server.js

220 lines
7.6 KiB
JavaScript

const express = require('express');
const {nanoid} = require('nanoid');
const { Pool } = require('pg');
const db = new Pool({
host: 'postgres',
database: 'pastebin',
user: 'pastebin',
password: 'buginoo'
});
// initialize app:
const app = express();
const port = 80;
db.query("CREATE TABLE IF NOT EXISTS bin (id VARCHAR PRIMARY KEY, name VARCHAR);"
+ "CREATE TABLE IF NOT EXISTS note (id VARCHAR PRIMARY KEY, text TEXT, modified TIMESTAMPTZ);"
+ "CREATE TABLE IF NOT EXISTS bin_note (bin_id VARCHAR REFERENCES bin (id), note_id VARCHAR REFERENCES note (id), PRIMARY KEY (bin_id, note_id));"
+ "CREATE TABLE IF NOT EXISTS user_ (id VARCHAR PRIMARY KEY, username TEXT, password TEXT);" // table 'user' is already taken
+ "CREATE TABLE IF NOT EXISTS bin_user (bin_id VARCHAR REFERENCES bin (id), user_id VARCHAR REFERENCES user_ (id), PRIMARY KEY (bin_id, user_id));"
+ "CREATE TABLE IF NOT EXISTS session (id VARCHAR PRIMARY KEY, user_id VARCHAR REFERENCES user_ (id));"
);
const router = express.Router();
const load_notes_stmt =
"SELECT n.id, n.text, n.modified FROM bin_note AS bn"
+" JOIN note AS n"
+" ON n.id = bn.note_id"
+" WHERE bn.bin_id = $1";
// {bin_id}
router.post('/load-notes', (req, res)=>{
const bin_id = req.body.bin_id;
db.query(load_notes_stmt, [bin_id])
.then(result => {
res.json({success:true, notes:result.rows})
});
});
const load_bin_stmt =
"SELECT b.id, b.name FROM bin AS b"
+" WHERE b.id = $1";
// {bin_id}
router.post('/load-bin', (req, res)=>{
const bin_id = req.body.bin_id;
db.query(load_bin_stmt, [bin_id])
.then(result => {
const bin = result.rows[0];
// if a bin with given id was found:
if(result.rows.length>0){
res.json({success:true, bin:{id:bin.id, name:bin.name}});
}
else{
res.json({success:false, bin:{id:bin_id, name:bin_id}});
}
});
// {status: 'ok', bin:{id:bin.id}, notes: bin.notes}
});
const search_stmt =
"SELECT n.id, n.text, n.modified FROM note AS n"
+" INNER JOIN bin_note AS bn"
+" ON bn.note_id = n.id AND bn.bin_id = $2"
+" WHERE n.text LIKE '%' || $1 || '%'"; // need to use string concat otherwise the `$1` is viewed as part of the string instead of a placeholder
const order_desc_stmt = " ORDER BY n.modified DESC";
const order_asc_stmt = " ORDER BY n.modified ASC";
// {search_term, sorting, bin_id}
router.post('/search', (req, res)=>{
const {search_term, bin_id, sorting} = req.body;
db.query(search_stmt+(sorting==='old->new'?order_asc_stmt:order_desc_stmt), [search_term, bin_id])
.then(result => {
res.json({success:true, notes:result.rows});
});
});
const upsert_note_stmt =
"INSERT INTO note (id, text, modified) VALUES ($1, $2, NOW())"
+" ON CONFLICT (id)"
+" DO UPDATE SET text = EXCLUDED.text, modified = EXCLUDED.modified";
const upsert_bin_stmt =
"INSERT INTO bin (id, name) VALUES ($1, $1)"
+" ON CONFLICT (id)"
+" DO NOTHING";
const upsert_bin_note_stmt =
"INSERT INTO bin_note (bin_id, note_id) VALUES ($1, $2)"
+" ON CONFLICT (bin_id, note_id)"
+" DO NOTHING";
const upsert_bin_user_stmt =
"INSERT INTO bin_user (bin_id, user_id)"
+" (SELECT $1, s.user_id FROM session AS s WHERE s.id = $2)"
+" ON CONFLICT (bin_id, user_id)"
+" DO NOTHING";
// {bin_id, note_id, text}
router.post('/save', (req, res)=>{
const {bin_id, note_id, text, session_id} = req.body;
db.connect().then(async (client)=>{
// TODO: make the following into a transaction:
await client.query(upsert_note_stmt, [note_id, text]);
await client.query(upsert_bin_stmt, [bin_id]);
await client.query(upsert_bin_note_stmt, [bin_id, note_id]);
// if the user is signed-in, ensure the bin and user are associated:
if(session_id !== ''){
await client.query(upsert_bin_user_stmt, [bin_id, session_id]);
}
// don't forget to release back into the Pool:
client.release();
res.json({success:true});
});
// {status: 'ok'}
});
const login_check_stmt =
"SELECT id, password FROM user_ AS u"
+" WHERE u.username = $1";
const register_stmt =
"INSERT INTO user_ (id, username, password) VALUES ($1, $2, $3)";
const session_create_stmt =
"INSERT INTO session (id, user_id) VALUES ($1, $2)";
const user_bin_list_stmt =
"SELECT b.id, b.name FROM bin_user AS bu"
+" INNER JOIN bin AS b"
+" ON bu.bin_id = b.id"
+" WHERE bu.user_id = $1";
router.post('/login', (req, res)=>{
const {username, password} = req.body;
const password_from_client=password;
db.query(login_check_stmt, [username])
.then(result=>{
// if there is such a username:
if(result.rows.length > 0){
const row = result.rows[0];
const user_id=row.id, password_from_db=row.password;
// if the passwords match:
// TODO: replace `===` with a constant-time comparison function to prevent timing attacks; or better, store passwords as salted hashes and use `===` on that:
if(password_from_client === password_from_db){
const session_id = nanoid();
db.query(session_create_stmt, [session_id, user_id]);
db.query(user_bin_list_stmt, [user_id])
.then(result=>{
const bins = result.rows;
res.json({success:true, user:{id:user_id, username}, session_id, bins});
});
}
else{
res.json({success: false});
}
}
// if no such user exists:
else{
const user_id = nanoid();
const session_id = nanoid();
db.query(register_stmt, [user_id, username, password_from_client])
.then(r=>{
db.query(session_create_stmt, [session_id, user_id]);
res.json({success: true, user:{id:user_id, username}, session_id, bins:[]});
});
}
});
});
const delete_session_stmt =
"DELETE FROM session WHERE id = $1";
router.post('/logout', (req, res)=>{
db.query(delete_session_stmt, [req.body.session_id]);
res.json({success:true});
});
const rename_bin_stmt =
"INSERT INTO bin (id, name) VALUES ($1, $2)"
+" ON CONFLICT (id)"
+" DO UPDATE SET name = EXCLUDED.name;";
router.post('/bin-rename', (req,res)=>{
const {bin_id, name, session_id} = req.body;
db.query(rename_bin_stmt, [bin_id, name])
.then(x=>{
// ensure user and bin are associated if necessary:
if(session_id !== ''){
// `upsert_bin_user_stmt` is defined above:
db.query(upsert_bin_user_stmt, [bin_id, session_id]);
}
res.json({success:true});
});
});
app.use('/', express.json(), router);
// run app:
const server = app.listen(port, () => {
console.log(`Pastebin app listening at http://localhost:${port}`)
});
// Ruthlessly copied from [https://medium.com/@becintec/building-graceful-node-applications-in-docker-4d2cd4d5d392]:
// The signals we want to handle
// NOTE: although it is tempting, the SIGKILL signal (9) cannot be intercepted and handled
var signals = {
'SIGHUP': 1,
'SIGINT': 2,
'SIGTERM': 15
};
// Do any necessary shutdown logic for our application here
const shutdown = (signal, value) => {
console.log("shutdown!");
server.close(() => {
console.log(`server stopped by ${signal} with value ${value}`);
process.exit(128 + value);
});
};
// Create a listener for each of the signals that we want to handle
Object.keys(signals).forEach((signal) => {
process.on(signal, () => {
console.log(`process received a ${signal} signal`);
shutdown(signal, signals[signal]);
});
});