From 9eb34fd46a8cefad8f6524094b6307ec02094f3e Mon Sep 17 00:00:00 2001 From: b1ek Date: Mon, 20 Feb 2023 12:43:53 +1000 Subject: [PATCH] add adminer and guestbook delete --- docker-compose.yml | 9 +++ gulpfile.js | 1 + helpers/index.js | 3 +- helpers/timesince.js | 29 +++++++ index.js | 10 ++- migrations/20230219070939-create-guestbook.js | 2 +- models/guestbook.js | 3 +- package.json | 1 + public/static/ui/gb_ui.css | 79 +++++++++++++++++++ routes/guestbook.js | 48 ++++++++++- view/guestbook.pug | 75 +++++++++++++++--- 11 files changed, 242 insertions(+), 18 deletions(-) create mode 100644 helpers/timesince.js diff --git a/docker-compose.yml b/docker-compose.yml index 493222a..0ca59a6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -32,6 +32,15 @@ services: - './data/db:/var/lib/postgresql' networks: - homepage + adminer: + image: adminer:standalone + ports: + - '8001:8080' + networks: + - homepage + environment: + ADMINER_DEFAULT_SERVER: postgres + ADMINER_DESIGN: rmsoft networks: homepage: diff --git a/gulpfile.js b/gulpfile.js index 4c0f9b1..83f0d81 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -15,6 +15,7 @@ function css(cb) { gulp.task("serve_dev", (cb) => { + console.log('Running in dev mode'); console.log('Launching node...'); let node = spawn('node', ['--inspect=0.0.0.0', 'index.js'], {stdio: 'inherit'}) diff --git a/helpers/index.js b/helpers/index.js index 3b2d98c..63b0308 100644 --- a/helpers/index.js +++ b/helpers/index.js @@ -1,3 +1,4 @@ module.exports = { - ViewLoader: require('./view_loader') + ViewLoader: require('./view_loader'), + TimeSince: require('./timesince') } \ No newline at end of file diff --git a/helpers/timesince.js b/helpers/timesince.js new file mode 100644 index 0000000..e04bcca --- /dev/null +++ b/helpers/timesince.js @@ -0,0 +1,29 @@ +function TimeSince(date) { + + var seconds = Math.floor((new Date() - date) / 1000); + + var interval = seconds / 31536000; + + if (interval > 1) { + return Math.floor(interval) + " years"; + } + interval = seconds / 2592000; + if (interval > 1) { + return Math.floor(interval) + " months"; + } + interval = seconds / 86400; + if (interval > 1) { + return Math.floor(interval) + " days"; + } + interval = seconds / 3600; + if (interval > 1) { + return Math.floor(interval) + " hours"; + } + interval = seconds / 60; + if (interval > 1) { + return Math.floor(interval) + " minutes"; + } + return Math.floor(seconds) + " seconds"; +} + +module.exports = TimeSince \ No newline at end of file diff --git a/index.js b/index.js index 651d146..6e7fd93 100644 --- a/index.js +++ b/index.js @@ -21,7 +21,15 @@ let RedisStore = require("connect-redis")(session) const { APP_PORT, APP_KEY } = process.env; - +app.use((req, res, next) => { + req.start = Date.now(); + res.on('header', (res) => { + let time = Date.now() - req.start; + console.log(time) + res.setHeader('X-Reponse-Time', time); + }) + next(); +}); app.use(bodyparser.json()); app.use(bodyparser.urlencoded({ extended: true })); app.use(cookie_parse(APP_KEY)) diff --git a/migrations/20230219070939-create-guestbook.js b/migrations/20230219070939-create-guestbook.js index 13ca22e..369d2f4 100644 --- a/migrations/20230219070939-create-guestbook.js +++ b/migrations/20230219070939-create-guestbook.js @@ -2,7 +2,7 @@ /** @type {import('sequelize-cli').Migration} */ module.exports = { async up(queryInterface, DataTypes) { - await queryInterface.createTable('Guestbooks', { + await queryInterface.createTable('guestbook', { id: { type: DataTypes.BIGINT(11), primaryKey: true, diff --git a/models/guestbook.js b/models/guestbook.js index f95eb14..39b581c 100644 --- a/models/guestbook.js +++ b/models/guestbook.js @@ -46,7 +46,8 @@ module.exports = (sequelize, DataTypes) => { } }, { sequelize, - modelName: 'Guestbook' + modelName: 'Guestbook', + tableName: 'guestbook' }); return Guestbook; }; \ No newline at end of file diff --git a/package.json b/package.json index 758d406..1e2ed6e 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "express-session": "^1.17.3", "glob": "^8.1.0", "gulp": "^4.0.2", + "html-escaper": "^3.0.3", "ioredis": "^5.3.1", "js-base64": "^3.7.5", "mocha": "^10.2.0", diff --git a/public/static/ui/gb_ui.css b/public/static/ui/gb_ui.css index a04187e..624e7ab 100644 --- a/public/static/ui/gb_ui.css +++ b/public/static/ui/gb_ui.css @@ -1,6 +1,26 @@ +/* entries */ +.gb_entries { + border: 0px solid #00000000; + border-collapse: collapse; + margin: 16px 10px; + width: 99% +} +.gb_entries tr { + border: 0 !important; + border-bottom: 1px solid #e2e4e2 !important; + transition: 250ms ease; +} +.gb_entries tr:hover { + background-color: #c2c4c220; +} +.gb_entries tr:target { + background-color: #91c29120; +} .gb_entry_text { padding: 0 8px; vertical-align: middle; + border-left: 1px solid #c2c4c2; + border-collapse: collapse; } .gb_entry_text .gb_entry_text_title { font-size:9pt; @@ -8,4 +28,63 @@ margin:0; padding:0; padding-top:1em +} +.gb_sender_data { + padding:0 8px; +} + +.gb_hidden_mail { + display:inline-block; + height:9.5pt; + border-radius:4px; + background:#666d65; + transform: translateY(1px); + border: 1px solid #4f534e; + box-sizing: border-box; + transition: 150ms ease; + font-size:7pt; + color: #eaeaea; + text-align: center; + user-select: none; + box-shadow: inset 0 1px 2px #fefefe20 1px 2px #30303040; + content: ''; + min-width: 100px; +} + +.gb_hidden_mail:hover { + background: #7e8b7b; +} +.gb_hidden_mail:hover::before { + content: 'Email is hidden'; +} + + + +/* ui elements */ + +/* delete record button */ +.gb_record_del_btn { + user-select: none; + box-shadow:inset 0px 1px 0px 0px #cf866c; + background:linear-gradient(to bottom, #d0451b 5%, #bc3315 100%); + background-color:#d0451b; + border-radius:6px; + border:1px solid #942911; + display:inline-block; + cursor:pointer; + color:#ffffff; + font-family:Arial; + font-size:12px; + font-weight:bold; + padding:2px 10px; + text-decoration:none; + text-shadow:0px 1px 0px #854629; +} +.gb_record_del_btn:hover { + background:linear-gradient(to bottom, #bc3315 5%, #d0451b 100%); + background-color:#bc3315; +} +.gb_record_del_btn:active { + position:relative; + top:1px; } \ No newline at end of file diff --git a/routes/guestbook.js b/routes/guestbook.js index 8353b27..0da6364 100644 --- a/routes/guestbook.js +++ b/routes/guestbook.js @@ -1,9 +1,21 @@ const Helpers = require('../helpers'); const Sequelize = require('../models'); +const html_escape = require('html-escaper'); + +const send_error = async (req, res, error, data) => { + res.send(await Helpers.ViewLoader.load('guestbook.pug', { + current_route: req.originalUrl, + ip: req.ip, + errors: error, + data + })); +}; async function handler(req, res, next) { try { + const errors = req.query.error; + let data = {}; let sqldata = await Sequelize.Guestbook.findAll({ where: { @@ -19,7 +31,8 @@ async function handler(req, res, next) { res.send(await Helpers.ViewLoader.load('guestbook.pug', { current_route: req.originalUrl, ip: req.ip, - data + data, + errors })); return; } catch (err) { @@ -40,14 +53,45 @@ async function submit(req, res, next) { hidden: false, time: Math.floor(Date.now() / 1000) }); - if (!data) next(new Error('Failed to create a new record.')); + if (!data) { + res.send(await Helpers.ViewLoader.load('guestbook.pug', { + current_route: req.originalUrl, + ip: req.ip, + errors: 'Could not create a new record' + })); + } res.redirect('/guestbook#gb_entry_' + data.id); return; } +async function del(req, res, next) { + try + { + let record = await Sequelize.Guestbook.findAndCountAll({ + where: {id: req.params.id} + }); + if (record.count == 0) { + res.redirect('/guestbook'); + } + const data = record.rows[0]; + if ( + data.ip == req.ip && + Math.floor(Date.now() / 1000) - data.time <= (60 * 60 * 24) + ) { + await Sequelize.Guestbook.update({hidden: true}, {where: {id: req.params.id}}) + res.redirect('/guestbook'); + } else { + res.redirect('/guestbook?error=' + encodeURIComponent('You don\'t have permission to delete this record.')) + return + } + } + catch (err) { next(err); } +} + module.exports = (router) => { router.get('/guestbook', handler); router.post('/guestbook/submit', submit); + router.get('/guestbook/del/:id', del); } \ No newline at end of file diff --git a/view/guestbook.pug b/view/guestbook.pug index 1413edf..2891364 100644 --- a/view/guestbook.pug +++ b/view/guestbook.pug @@ -1,6 +1,34 @@ extends layout/main.pug block root - var title = 'Guestbook' + - + function TimeSince(date) { + + var seconds = Math.floor((new Date() - date) / 1000); + + var interval = seconds / 31536000; + + if (interval > 1) { + return Math.floor(interval) + " years"; + } + interval = seconds / 2592000; + if (interval > 1) { + return Math.floor(interval) + " months"; + } + interval = seconds / 86400; + if (interval > 1) { + return Math.floor(interval) + " days"; + } + interval = seconds / 3600; + if (interval > 1) { + return Math.floor(interval) + " hours"; + } + interval = seconds / 60; + if (interval > 1) { + return Math.floor(interval) + " minutes"; + } + return Math.floor(seconds) + " seconds"; + } block head @@ -18,19 +46,26 @@ block content tr td Your name: td - input(type='text' name='name' value='John Doe') + input(type='text' name='name' value='' style='width:50%') + span(style='font-size:9pt;color:red;user-select:none' title='required') * tr td Your email: td - input(type='email' name='email' value='john.doe@example.com') + input(type='email' name='email' value='') tr td Hide your email? td input(type='checkbox' name='hidemail') - p(style='margin:6px 0') Your message (512 chars max): + // span(style='font-size:9pt;color:red;user-select:none' title='required') * + p(style='margin:6px 0') + | Your message (512 chars max): + span(style='font-size:9pt;color:red;user-select:none' title='required') * textarea(name='message' style='width:100%;height:150px;max-width:600px;max-height:300px') p input(type='submit' class='send_button_1') + if (errors) + br + span(style='font-weight:bold;color:darkred;font-size:9pt') !{errors} td(style='padding:0 16px;margin:0') h5 Guidelines ul @@ -42,22 +77,38 @@ block content span(style='font-size:10pt;color:darkred;font-weight:bold'). Warning: Your ip (#{ip}) will be logged and displayed for everyone.
You can delete your own message if it was sent from the same ip for 24 hours after it was sent. + p + span(style='font-size:9pt;color:red;user-select:none' title='required') * + | - required hr if (!data) p No records available. else - table + table(class='gb_entries') each entry, id in data - tr + tr(id='gb_entry_' + id) td(width='20%' class='gb_sender_data') - p(style='font-size:9pt'). - ID: ##{id}
- Sender: #{entry.name}
- Email: #{entry.email}
- IP: #{entry.ip}
- // Date: #{new Date(entry.time).toISOString()}
+ p(style='font-size:9pt') + | ID: + a(href='gb_entry_' + entry.id) ##{entry.id} + br + | Sender: #{entry.name} + br + if (!entry.hidemail) + | Email: #{entry.email} + else + | Email: + span(class='gb_hidden_mail' style='width:' + (10 * entry.email.length) + 'px') + br + | IP: #{entry.ip} + br + | Date: #{TimeSince(new Date(entry.time * 1000))} ago + + if (ip == entry.ip && Math.floor(Date.now() / 1000) - entry.time <= (60 * 60 * 24)) + p(style='margin:0;padding:0;padding-bottom:12px') + a(href='/guestbook/del/' + id class='gb_record_del_btn' title='you can delete your own messages') delete td(width='80%' class='gb_entry_text') p(class='gb_entry_text_title') Message: p(style='margin:0;padding:0;font-size:10pt'). - hiii \ No newline at end of file + #{entry.text} \ No newline at end of file