Added opt-in option for promotional emails

This commit is contained in:
2021-02-11 16:01:39 +11:00
parent 7759a1cd40
commit 615b686890
19 changed files with 848 additions and 83 deletions
+1
View File
@@ -5,6 +5,7 @@ WEB_PORT=3000
MAIL_SMTP=smtp.example.com
MAIL_USERNAME=foobar@example.com
MAIL_PASSWORD=foobar
MAIL_PHYSICAL=42 Placeholder Ave, Placeholder, 0000, USA
DB_HOSTNAME=127.0.0.1
DB_DATABASE=template
+2 -2
View File
@@ -44,8 +44,8 @@ There are external components to this template referred to as "microservices". T
# TODO list
- Legal Requirements:
- Physical Mailing Address Config (for emails)
- Opt-out option (for emails)
- ~~Physical Mailing Address Config (for emails)~~
- ~~Opt-out option (for emails)~~
- Information about legal requirements of the developers using this template
- Privacy policy & data collection notices
- LICENSE file
+43 -2
View File
@@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React, { useEffect } from 'react';
import { Redirect } from 'react-router-dom';
import { useCookies } from 'react-cookie';
@@ -12,12 +12,53 @@ const Account = props => {
return <Redirect to='/' />;
}
//refs
let contactElement;
//once before render
useEffect(() => {
fetch('/api/accounts')
.then(blob => blob.json())
.then(json => {
contactElement.checked = json.contact;
})
.catch(e => console.error(e))
;
}, []);
return (
<div className='page'>
<h1 className='centered'>Account</h1>
<DeleteAccount />
<form className='constricted' onSubmit={async evt => {
evt.preventDefault();
await update(contactElement.checked);
}}>
<div>
<label htmlFor='contact'>Allow Promotional Emails:</label>
<input type='checkbox' name='contact' ref={e => contactElement = e} />
</div>
<button type='submit'>Update Information</button>
</form>
<DeleteAccount className='constricted' />
</div>
);
};
const update = async (contact) => {
//generate a new formdata payload
let formData = new FormData();
formData.append('contact', contact);
const result = await fetch('/api/accounts', { method: 'PATCH', body: formData });
if (result.ok) {
alert(await result.text());
} else {
alert(await result.text());
}
}
export default Account;
+19 -12
View File
@@ -15,7 +15,7 @@ const SignUp = props => {
}
//refs
let emailElement, usernameElement, passwordElement, retypeElement;
let emailElement, usernameElement, passwordElement, retypeElement, contactElement;
return (
<div className='page'>
@@ -23,32 +23,38 @@ const SignUp = props => {
<form className='constricted' onSubmit={
evt => {
evt.preventDefault();
handleSubmit(emailElement.value, usernameElement.value, passwordElement.value, retypeElement.value)
handleSubmit(emailElement.value, usernameElement.value, passwordElement.value, retypeElement.value, contactElement.checked)
.then(res => res ? alert(res) : null)
.then(() => emailElement.value = usernameElement.value = passwordElement.value = retypeElement.value = '') //clear input
.then(() => contactElement.checked = false)
.then(() => props.history.push('/'))
.catch(e => console.error(e))
;
}
}>
<div>
<label htmlFor="email">Email:</label>
<input type="email" name="email" ref={e => emailElement = e} />
<label htmlFor='email'>Email:</label>
<input type='email' name='email' ref={e => emailElement = e} />
</div>
<div>
<label htmlFor="username">Username:</label>
<input type="text" name="username" ref={e => usernameElement = e} />
<label htmlFor='username'>Username:</label>
<input type='text' name='username' ref={e => usernameElement = e} />
</div>
<div>
<label htmlFor="password">Password:</label>
<input type="password" name="password" ref={e => passwordElement = e} />
<label htmlFor='password'>Password:</label>
<input type='password' name='password' ref={e => passwordElement = e} />
</div>
<div>
<label htmlFor="retype">Retype Password:</label>
<input type="password" name="retype" ref={e => retypeElement = e} />
<label htmlFor='retype'>Retype Password:</label>
<input type='password' name='retype' ref={e => retypeElement = e} />
</div>
<div>
<label htmlFor='contact'>Allow Promotional Emails:</label>
<input type='checkbox' name='contact' ref={e => contactElement = e} />
</div>
<button type='submit'>Signup</button>
@@ -57,12 +63,12 @@ const SignUp = props => {
);
};
const handleSubmit = async (email, username, password, retype) => {
const handleSubmit = async (email, username, password, retype, contact) => {
email = email.trim();
username = username.trim();
const err = handleValidation(email, username, password, retype);
if (err) {
return err;
}
@@ -73,6 +79,7 @@ const handleSubmit = async (email, username, password, retype) => {
formData.append('email', email);
formData.append('username', username);
formData.append('password', password);
formData.append('contact', contact)
const result = await fetch('/api/accounts/signup', { method: 'POST', body: formData });
+3 -3
View File
@@ -5,13 +5,13 @@ const DeleteAccount = props => {
const [open, setOpen] = useState(false);
if (!open) {
return <button onClick={() => setOpen(true)}>Delete Account</button>
return <button onClick={() => setOpen(true)} className={props.className}>Delete Account</button>
}
let passwordElement;
return (
<form className='constricted' onSubmit={async evt => {
<form className={props.className} onSubmit={async evt => {
evt.preventDefault();
const password = passwordElement.value;
passwordElement.value = '';
@@ -34,7 +34,7 @@ const handleSubmit = async (password) => {
formData.append('password', password);
const result = await fetch('/api/accounts/deletion', { method: 'POST', body: formData });
const result = await fetch('/api/accounts/deletion', { method: 'DELETE', body: formData });
if (!result.ok) {
alert(await result.text());
+3 -1
View File
@@ -25,6 +25,7 @@ const question = (prompt, def) => {
const projectMailSMTP = await question('Project Mail SMTP', 'smtp.example.com');
const projectMailUser = await question('Project Mail Username', 'foobar@example.com');
const projectMailPass = await question('Project Mail Password', 'foobar');
const projectMailPhysical = await question('Project Physical Mailing Address', '[Unknown]');
const projectDBUser = await question('Project Database Username', projectName);
const projectDBPass = await question('Project Database Password', 'pikachu');
@@ -64,7 +65,8 @@ services:
- WEB_PORT=3000
- MAIL_SMTP=${projectMailSMTP}
- MAIL_USERNAME=${projectMailUser}
- MAIL_PASSWORD=${projectMailPass}
- MAIL_PASSWORD=${projectMailPass}
- MAIL_PHYSICAL=${projectMailPhysical}
- DB_HOSTNAME=database
- DB_DATABASE=${projectName}
- DB_USERNAME=${projectDBUser}
+658 -31
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -26,6 +26,7 @@
"homepage": "https://github.com/KRGameStudios/MERN-template#readme",
"dependencies": {
"bcryptjs": "^2.4.3",
"connect-session-sequelize": "^7.1.0",
"cookie-parser": "^1.4.5",
"core-js": "^3.8.3",
"dateformat": "^4.5.1",
+7 -1
View File
@@ -2,10 +2,16 @@ const express = require('express');
const router = express.Router();
//basic account management
router.get('/', require('./query'));
router.patch('/', require('./update'));
//signup -> login -> logout
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'));
//account deletion
router.delete('/deletion', require('./deletion'));
module.exports = router;
+1 -1
View File
@@ -37,7 +37,7 @@ const route = async (req, res) => {
}
//save the session and cookie data
req.session.account = account;
req.session.account = JSON.parse(JSON.stringify(account.dataValues));
res.cookie('loggedin', process.env.WEB_ADDRESS);
if (account.privilege == 'administrator') {
+21
View File
@@ -0,0 +1,21 @@
const { accounts } = require('../database/models');
const route = async (req, res) => {
if (!req.session.account || !req.session.account.id) {
res.status(401).send('Unknown account');
}
//update the reference
req.session.account = (await accounts.findOne({
where: {
id: req.session.account.id
}
})).dataValues;
//respond with the private-facing data
res.status(200).json({
contact: req.session.account.contact
});
};
module.exports = route;
+8 -2
View File
@@ -101,6 +101,7 @@ const registerPendingSignup = async (fields, hash, token) => {
email: fields.email,
username: fields.username,
hash: hash,
contact: fields.contact,
token: token
});
@@ -109,7 +110,12 @@ const registerPendingSignup = async (fields, hash, token) => {
const sendValidationEmail = async (email, username, token) => {
const addr = `${process.env.WEB_PROTOCOL}://${process.env.WEB_ADDRESS}/api/accounts/validation?username=${username}&token=${token}`;
const msg = `Hello! Please visit the following address to validate your account: ${addr}`;
const msg = `Hello ${username}!
Please visit the following link to validate your account: ${addr}
You can contact us directly at our physical mailing address here: ${process.env.MAIL_PHYSICAL}
`;
let transporter, info;
@@ -143,7 +149,7 @@ const sendValidationEmail = async (email, username, token) => {
}
if (info.accepted[0] != email) {
return 'validation email failed';
return 'validation email failed to send';
}
return null;
+28
View File
@@ -0,0 +1,28 @@
const { accounts } = require('../database/models');
const route = async (req, res) => {
if (!req.session.account.id) {
return res.status(500).send('missing account data');
}
//update the account
await accounts.update({
contact: req.fields.contact
}, {
where: {
id: req.session.account.id
}
});
//update the reference
req.session.account = (await accounts.findOne({
where: {
id: req.session.account.id
}
})).dataValues;
//respond with an OK
res.status(200).send('Information updated');
};
module.exports = route;
+2 -1
View File
@@ -29,7 +29,8 @@ const route = async (req, res) => {
accounts.create({
email: info.email,
username: info.username,
hash: info.hash
hash: info.hash,
contact: info.contact
});
//finally
+25
View File
@@ -0,0 +1,25 @@
//DOCS: this whole file is just a big bugfix
//DOCS: ensure that there is at least one administration account
const bcrypt = require('bcryptjs');
const { accounts } = require('../database/models');
const defaultAdminAccount = async () => {
const admin = await accounts.findOne({
where: {
privilege: 'administrator'
}
});
if (admin == null) {
await accounts.create({
privilege: 'administrator',
email: `admin@${process.env.WEB_ADDRESS}`,
username: `admin`,
hash: await bcrypt.hash('password', await bcrypt.genSalt(11))
});
console.log(`Created default admin account (email: admin@${process.env.WEB_ADDRESS}; password: password)`);
}
};
module.exports = defaultAdminAccount;
-23
View File
@@ -16,27 +16,4 @@ router.get('/banned', require('./banned'));
router.post('/ban', require('./ban'));
router.post('/unban', require('./unban'));
//DOCS: ensure that there is at least one administration account
const bcrypt = require('bcryptjs');
const { accounts } = require('../database/models');
(async () => {
const admin = await accounts.findOne({
where: {
privilege: 'administrator'
}
});
if (admin == null) {
await accounts.create({
privilege: 'administrator',
email: `admin@${process.env.WEB_ADDRESS}`,
username: `admin`,
hash: await bcrypt.hash('password', await bcrypt.genSalt(11))
});
console.log(`Created default admin account (email: admin@${process.env.WEB_ADDRESS}; password: password)`);
}
})();
module.exports = router;
+6
View File
@@ -28,6 +28,12 @@ module.exports = sequelize.define('accounts', {
hash: 'varchar(100)', //for passwords
contact: {
type: Sequelize.BOOLEAN,
allowNull: false,
defaultValue: false
},
deletion: {
type: 'DATETIME',
allowNull: true,
@@ -14,5 +14,11 @@ module.exports = sequelize.define('pendingSignups', {
hash: 'varchar(100)', //for passwords
contact: {
type: Sequelize.BOOLEAN,
allowNull: false,
defaultValue: false
},
token: Sequelize.INTEGER(11)
});
+14 -4
View File
@@ -11,19 +11,29 @@ const path = require('path');
const formidable = require('express-formidable');
const cookieParser = require('cookie-parser');
const session = require('express-session');
app.use(formidable());
app.use(cookieParser());
app.use(session({ secret: process.env.SESSION_SECRET, resave: true, saveUninitialized: true }));
const SequelizeStore = require("connect-session-sequelize")(session.Store);
//database connection
const database = require('./database');
const models = require('./database/models'); //invoke all models
app.use(formidable());
app.use(cookieParser());
app.use(session({
secret: process.env.SESSION_SECRET,
resave: true,
saveUninitialized: true,
store: new SequelizeStore({
db: database
})
}));
//account management
app.use('/api/accounts', require('./accounts'));
//administration
app.use('/api/admin', require('./admin'));
require('./admin/bookkeeper')(); //BUGFIX
//send static files
app.use('/', express.static(path.resolve(__dirname, '..', 'public')));