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" +" WHERE n.text LIKE '%' || $1 || '%'" // need to use string concat otherwise the `$1` is viewed as part of the string instead of a placeholder +" ORDER BY n.modified DESC"; // {search_term, sorting, bin_id} router.post('/search', (req, res)=>{ const search_term = req.body.search_term; db.query(search_stmt, [search_term]) .then(result => { res.json({status:'ok', notes:result.rows}); }); // {status: 'ok', notes} }); 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]); }); });