add adminer and guestbook delete

This commit is contained in:
b1ek 2023-02-20 12:43:53 +10:00
parent 7f674379d2
commit 9eb34fd46a
11 changed files with 242 additions and 18 deletions

View File

@ -32,6 +32,15 @@ services:
- './data/db:/var/lib/postgresql' - './data/db:/var/lib/postgresql'
networks: networks:
- homepage - homepage
adminer:
image: adminer:standalone
ports:
- '8001:8080'
networks:
- homepage
environment:
ADMINER_DEFAULT_SERVER: postgres
ADMINER_DESIGN: rmsoft
networks: networks:
homepage: homepage:

View File

@ -15,6 +15,7 @@ function css(cb) {
gulp.task("serve_dev", (cb) => { gulp.task("serve_dev", (cb) => {
console.log('Running in dev mode');
console.log('Launching node...'); console.log('Launching node...');
let node = spawn('node', ['--inspect=0.0.0.0', 'index.js'], {stdio: 'inherit'}) let node = spawn('node', ['--inspect=0.0.0.0', 'index.js'], {stdio: 'inherit'})

View File

@ -1,3 +1,4 @@
module.exports = { module.exports = {
ViewLoader: require('./view_loader') ViewLoader: require('./view_loader'),
TimeSince: require('./timesince')
} }

29
helpers/timesince.js Normal file
View File

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

View File

@ -21,7 +21,15 @@ let RedisStore = require("connect-redis")(session)
const { APP_PORT, APP_KEY } = process.env; 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.json());
app.use(bodyparser.urlencoded({ extended: true })); app.use(bodyparser.urlencoded({ extended: true }));
app.use(cookie_parse(APP_KEY)) app.use(cookie_parse(APP_KEY))

View File

@ -2,7 +2,7 @@
/** @type {import('sequelize-cli').Migration} */ /** @type {import('sequelize-cli').Migration} */
module.exports = { module.exports = {
async up(queryInterface, DataTypes) { async up(queryInterface, DataTypes) {
await queryInterface.createTable('Guestbooks', { await queryInterface.createTable('guestbook', {
id: { id: {
type: DataTypes.BIGINT(11), type: DataTypes.BIGINT(11),
primaryKey: true, primaryKey: true,

View File

@ -46,7 +46,8 @@ module.exports = (sequelize, DataTypes) => {
} }
}, { }, {
sequelize, sequelize,
modelName: 'Guestbook' modelName: 'Guestbook',
tableName: 'guestbook'
}); });
return Guestbook; return Guestbook;
}; };

View File

@ -22,6 +22,7 @@
"express-session": "^1.17.3", "express-session": "^1.17.3",
"glob": "^8.1.0", "glob": "^8.1.0",
"gulp": "^4.0.2", "gulp": "^4.0.2",
"html-escaper": "^3.0.3",
"ioredis": "^5.3.1", "ioredis": "^5.3.1",
"js-base64": "^3.7.5", "js-base64": "^3.7.5",
"mocha": "^10.2.0", "mocha": "^10.2.0",

View File

@ -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 { .gb_entry_text {
padding: 0 8px; padding: 0 8px;
vertical-align: middle; vertical-align: middle;
border-left: 1px solid #c2c4c2;
border-collapse: collapse;
} }
.gb_entry_text .gb_entry_text_title { .gb_entry_text .gb_entry_text_title {
font-size:9pt; font-size:9pt;
@ -8,4 +28,63 @@
margin:0; margin:0;
padding:0; padding:0;
padding-top:1em 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;
} }

View File

@ -1,9 +1,21 @@
const Helpers = require('../helpers'); const Helpers = require('../helpers');
const Sequelize = require('../models'); 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) { async function handler(req, res, next) {
try { try {
const errors = req.query.error;
let data = {}; let data = {};
let sqldata = await Sequelize.Guestbook.findAll({ let sqldata = await Sequelize.Guestbook.findAll({
where: { where: {
@ -19,7 +31,8 @@ async function handler(req, res, next) {
res.send(await Helpers.ViewLoader.load('guestbook.pug', { res.send(await Helpers.ViewLoader.load('guestbook.pug', {
current_route: req.originalUrl, current_route: req.originalUrl,
ip: req.ip, ip: req.ip,
data data,
errors
})); }));
return; return;
} catch (err) { } catch (err) {
@ -40,14 +53,45 @@ async function submit(req, res, next) {
hidden: false, hidden: false,
time: Math.floor(Date.now() / 1000) 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); res.redirect('/guestbook#gb_entry_' + data.id);
return; 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) => { module.exports = (router) => {
router.get('/guestbook', handler); router.get('/guestbook', handler);
router.post('/guestbook/submit', submit); router.post('/guestbook/submit', submit);
router.get('/guestbook/del/:id', del);
} }

View File

@ -1,6 +1,34 @@
extends layout/main.pug extends layout/main.pug
block root block root
- var title = 'Guestbook' - 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 block head
<link rel='stylesheet' href='/static/ui/send_button.css'> <link rel='stylesheet' href='/static/ui/send_button.css'>
<link rel='stylesheet' href='/static/ui/gb_ui.css'> <link rel='stylesheet' href='/static/ui/gb_ui.css'>
@ -18,19 +46,26 @@ block content
tr tr
td Your name: td Your name:
td 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 tr
td Your email: td Your email:
td td
input(type='email' name='email' value='john.doe@example.com') input(type='email' name='email' value='')
tr tr
td Hide your email? td Hide your email?
td td
input(type='checkbox' name='hidemail') 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') textarea(name='message' style='width:100%;height:150px;max-width:600px;max-height:300px')
p p
input(type='submit' class='send_button_1') 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') td(style='padding:0 16px;margin:0')
h5 Guidelines h5 Guidelines
ul ul
@ -42,22 +77,38 @@ block content
span(style='font-size:10pt;color:darkred;font-weight:bold'). span(style='font-size:10pt;color:darkred;font-weight:bold').
Warning: Your ip (#{ip}) will be logged and displayed for everyone.<br/> Warning: Your ip (#{ip}) will be logged and displayed for everyone.<br/>
You can delete your own message if it was sent from the same ip for 24 hours after it was sent. 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 hr
if (!data) if (!data)
p No records available. p No records available.
else else
table table(class='gb_entries')
each entry, id in data each entry, id in data
tr tr(id='gb_entry_' + id)
td(width='20%' class='gb_sender_data') td(width='20%' class='gb_sender_data')
p(style='font-size:9pt'). p(style='font-size:9pt')
ID: <a id='gb_entry_#{id}' href='#gb_entry_#{id}'>##{id}</a><br/> | ID:
Sender: #{entry.name}<br/> a(href='gb_entry_' + entry.id) ##{entry.id}
Email: #{entry.email}<br/> br
IP: #{entry.ip}<br/> | Sender: #{entry.name}
// Date: #{new Date(entry.time).toISOString()}<br/> 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') td(width='80%' class='gb_entry_text')
p(class='gb_entry_text_title') Message: p(class='gb_entry_text_title') Message:
p(style='margin:0;padding:0;font-size:10pt'). p(style='margin:0;padding:0;font-size:10pt').
hiii #{entry.text}