diff --git a/.envdev b/.envdev index 47dfd87..9c6b294 100644 --- a/.envdev +++ b/.envdev @@ -10,3 +10,4 @@ DB_HOSTNAME=127.0.0.1 DB_DATABASE=template DB_USERNAME=template DB_PASSWORD=pikachu +DB_TIMEZONE=Australia/Sydney \ No newline at end of file diff --git a/README.md b/README.md index 6623309..d705e62 100644 --- a/README.md +++ b/README.md @@ -23,8 +23,9 @@ This should get the template working in development mode. - ~~validate email~~ - ~~login (with cookies)~~ - ~~logout (with cookies)~~ - - account deletion and management + - ~~account deletion~~ - annoying "This site uses cookies" message + - CSS template? - Administration Panel - Exclusive to admin accounts - ban/unban accounts diff --git a/client/components/pages/account.jsx b/client/components/pages/account.jsx index 3d90fc1..11cf8e0 100644 --- a/client/components/pages/account.jsx +++ b/client/components/pages/account.jsx @@ -1,9 +1,21 @@ -import React from 'react'; +import React, { useState } from 'react'; +import { Redirect } from 'react-router-dom'; +import { useCookies } from 'react-cookie'; + +import DeleteAccount from '../panels/delete-account'; const Account = props => { + const [cookies, setCookie] = useCookies(['loggedin']); + + //check for logged in redirect + if (!cookies['loggedin']) { + return ; + } + return (
-

Account

+

Account

+
); }; diff --git a/client/components/pages/login.jsx b/client/components/pages/login.jsx index 5232812..6971cfc 100644 --- a/client/components/pages/login.jsx +++ b/client/components/pages/login.jsx @@ -18,7 +18,7 @@ const LogIn = props => { return (
-

Login

+

Login

{ evt.preventDefault(); diff --git a/client/components/pages/signup.jsx b/client/components/pages/signup.jsx index 5611781..8369718 100644 --- a/client/components/pages/signup.jsx +++ b/client/components/pages/signup.jsx @@ -19,7 +19,7 @@ const SignUp = props => { return (
-

Signup

+

Signup

{ evt.preventDefault(); diff --git a/client/components/panels/delete-account.jsx b/client/components/panels/delete-account.jsx new file mode 100644 index 0000000..6232b0b --- /dev/null +++ b/client/components/panels/delete-account.jsx @@ -0,0 +1,51 @@ +import React, { useState } from 'react'; + +//DOCS: isolated the delete account button into it's own panel, so it can be easily moved as needed +const DeleteAccount = props => { + const [open, setOpen] = useState(false); + + if (!open) { + return + } + + let passwordElement; + + return ( + { + evt.preventDefault(); + const password = passwordElement.value; + passwordElement.value = ''; + await handleSubmit(password); + }}> +
+ + passwordElement = e} /> +
+ + + + + ); +}; + +const handleSubmit = async (password) => { + //generate a new formdata payload + let formData = new FormData(); + + formData.append('password', password); + + const result = await fetch('/api/accounts/deletion', { method: 'POST', body: formData }); + + if (!result.ok) { + alert(await result.text()); + } else { + //force logout + fetch('/api/accounts/logout', { method: 'POST' }) + .then(alert(await result.text())) + .then(() => window.location.reload(true)) //BUFGIX: force reload of the header element + .catch(e => console.error(e)) + ; + } +}; + +export default DeleteAccount; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 56844cf..1dd6260 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "express-formidable": "^1.2.0", "express-session": "^1.17.1", "mariadb": "^2.5.2", + "node-cron": "^2.0.3", "nodemailer": "^6.4.17", "react-cookie": "^4.0.3", "regenerator-runtime": "^0.13.7", @@ -5722,6 +5723,19 @@ "integrity": "sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==", "dev": true }, + "node_modules/node-cron": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-2.0.3.tgz", + "integrity": "sha512-eJI+QitXlwcgiZwNNSRbqsjeZMp5shyajMR81RZCqeW0ZDEj4zU9tpd4nTh/1JsBiKbF8d08FCewiipDmVIYjg==", + "hasInstallScript": true, + "dependencies": { + "opencollective-postinstall": "^2.0.0", + "tz-offset": "0.0.1" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/node-forge": { "version": "0.10.0", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz", @@ -5986,6 +6000,14 @@ "node": ">=6" } }, + "node_modules/opencollective-postinstall": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/opencollective-postinstall/-/opencollective-postinstall-2.0.3.tgz", + "integrity": "sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q==", + "bin": { + "opencollective-postinstall": "index.js" + } + }, "node_modules/opener": { "version": "1.5.2", "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", @@ -8061,6 +8083,11 @@ "is-typedarray": "^1.0.0" } }, + "node_modules/tz-offset": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/tz-offset/-/tz-offset-0.0.1.tgz", + "integrity": "sha512-kMBmblijHJXyOpKzgDhKx9INYU4u4E1RPMB0HqmKSgWG8vEcf3exEfLh4FFfzd3xdQOw9EuIy/cP0akY6rHopQ==" + }, "node_modules/uid-safe": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", @@ -14298,6 +14325,15 @@ } } }, + "node-cron": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-2.0.3.tgz", + "integrity": "sha512-eJI+QitXlwcgiZwNNSRbqsjeZMp5shyajMR81RZCqeW0ZDEj4zU9tpd4nTh/1JsBiKbF8d08FCewiipDmVIYjg==", + "requires": { + "opencollective-postinstall": "^2.0.0", + "tz-offset": "0.0.1" + } + }, "node-forge": { "version": "0.10.0", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz", @@ -14503,6 +14539,11 @@ "mimic-fn": "^2.1.0" } }, + "opencollective-postinstall": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/opencollective-postinstall/-/opencollective-postinstall-2.0.3.tgz", + "integrity": "sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q==" + }, "opener": { "version": "1.5.2", "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", @@ -16219,6 +16260,11 @@ "is-typedarray": "^1.0.0" } }, + "tz-offset": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/tz-offset/-/tz-offset-0.0.1.tgz", + "integrity": "sha512-kMBmblijHJXyOpKzgDhKx9INYU4u4E1RPMB0HqmKSgWG8vEcf3exEfLh4FFfzd3xdQOw9EuIy/cP0akY6rHopQ==" + }, "uid-safe": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", diff --git a/package.json b/package.json index cc11ec9..2572df2 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "express-formidable": "^1.2.0", "express-session": "^1.17.1", "mariadb": "^2.5.2", + "node-cron": "^2.0.3", "nodemailer": "^6.4.17", "react-cookie": "^4.0.3", "regenerator-runtime": "^0.13.7", diff --git a/server/accounts/deletion.js b/server/accounts/deletion.js new file mode 100644 index 0000000..8896db5 --- /dev/null +++ b/server/accounts/deletion.js @@ -0,0 +1,52 @@ +//libraries +const utils = require('util'); +const bcrypt = require('bcryptjs'); +var cron = require('node-cron'); + +const Sequelize = require('sequelize'); +const Op = Sequelize.Op; +const { accounts } = require('../database/models'); + +//api/accounts/deletion +const route = async (req, res) => { + //make sure the account is logged in + if (req.cookies['loggedin'] !== process.env.WEB_ADDRESS) { + return res.status(401).send('invalid session status'); + } + + //compare the user's password + const compare = utils.promisify(bcrypt.compare); + const match = await compare(req.fields.password, req.session.account.hash); + + if (!match) { + return res.status(401).send('incorrect password'); + } + + //set the deletion time (2 days from now) + const interval = new Date(new Date().setDate(new Date().getDate() + 2)); //wow + await accounts.update({ + deletion: interval + }, + { + where: { + id: req.session.account.id + } + }); + + //finally + return res.status(200).send('account will be deleted in two days - log in to cancel'); +}; + +//actually delete the accounts +cron.schedule('0 * * * *', () => { + console.log('wiping accounts'); + accounts.destroy({ + where: { + deletion: { + [Op.lt]: Sequelize.fn('NOW') + } + } + }); +}); + +module.exports = route; \ No newline at end of file diff --git a/server/accounts/index.js b/server/accounts/index.js index ac5021d..dda4dd2 100644 --- a/server/accounts/index.js +++ b/server/accounts/index.js @@ -6,5 +6,6 @@ router.post('/signup', require('./signup')); router.get('/validation', require('./validation')); router.post('/login', require('./login')); router.post('/logout', require('./logout')); +router.post('/deletion', require('./deletion')); module.exports = router; diff --git a/server/accounts/login.js b/server/accounts/login.js index ae09a63..07a2095 100644 --- a/server/accounts/login.js +++ b/server/accounts/login.js @@ -40,6 +40,13 @@ const route = async (req, res) => { req.session.account = account; res.cookie('loggedin', process.env.WEB_ADDRESS); + //cancel deletion if any + await accounts.update({ deletion: null }, { + where: { + id: account.id + } + }); + //finally res.status(200).send('login succeeded'); }; diff --git a/server/database/index.js b/server/database/index.js index fadb96a..bde6631 100644 --- a/server/database/index.js +++ b/server/database/index.js @@ -3,6 +3,7 @@ const Sequelize = require('sequelize'); const sequelize = new Sequelize(process.env.DB_DATABASE, process.env.DB_USERNAME, process.env.DB_PASSWORD, { host: process.env.DB_HOSTADDR, dialect: 'mariadb', + timezone: process.env.DB_TIMEZONE, logging: false });