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