Added opt-in option for promotional emails
This commit is contained in:
@@ -5,6 +5,7 @@ WEB_PORT=3000
|
|||||||
MAIL_SMTP=smtp.example.com
|
MAIL_SMTP=smtp.example.com
|
||||||
MAIL_USERNAME=foobar@example.com
|
MAIL_USERNAME=foobar@example.com
|
||||||
MAIL_PASSWORD=foobar
|
MAIL_PASSWORD=foobar
|
||||||
|
MAIL_PHYSICAL=42 Placeholder Ave, Placeholder, 0000, USA
|
||||||
|
|
||||||
DB_HOSTNAME=127.0.0.1
|
DB_HOSTNAME=127.0.0.1
|
||||||
DB_DATABASE=template
|
DB_DATABASE=template
|
||||||
|
|||||||
@@ -44,8 +44,8 @@ There are external components to this template referred to as "microservices". T
|
|||||||
# TODO list
|
# TODO list
|
||||||
|
|
||||||
- Legal Requirements:
|
- Legal Requirements:
|
||||||
- Physical Mailing Address Config (for emails)
|
- ~~Physical Mailing Address Config (for emails)~~
|
||||||
- Opt-out option (for emails)
|
- ~~Opt-out option (for emails)~~
|
||||||
- Information about legal requirements of the developers using this template
|
- Information about legal requirements of the developers using this template
|
||||||
- Privacy policy & data collection notices
|
- Privacy policy & data collection notices
|
||||||
- LICENSE file
|
- LICENSE file
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { Redirect } from 'react-router-dom';
|
import { Redirect } from 'react-router-dom';
|
||||||
import { useCookies } from 'react-cookie';
|
import { useCookies } from 'react-cookie';
|
||||||
|
|
||||||
@@ -12,12 +12,53 @@ const Account = props => {
|
|||||||
return <Redirect to='/' />;
|
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 (
|
return (
|
||||||
<div className='page'>
|
<div className='page'>
|
||||||
<h1 className='centered'>Account</h1>
|
<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>
|
</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;
|
export default Account;
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ const SignUp = props => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//refs
|
//refs
|
||||||
let emailElement, usernameElement, passwordElement, retypeElement;
|
let emailElement, usernameElement, passwordElement, retypeElement, contactElement;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='page'>
|
<div className='page'>
|
||||||
@@ -23,32 +23,38 @@ const SignUp = props => {
|
|||||||
<form className='constricted' onSubmit={
|
<form className='constricted' onSubmit={
|
||||||
evt => {
|
evt => {
|
||||||
evt.preventDefault();
|
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(res => res ? alert(res) : null)
|
||||||
.then(() => emailElement.value = usernameElement.value = passwordElement.value = retypeElement.value = '') //clear input
|
.then(() => emailElement.value = usernameElement.value = passwordElement.value = retypeElement.value = '') //clear input
|
||||||
|
.then(() => contactElement.checked = false)
|
||||||
.then(() => props.history.push('/'))
|
.then(() => props.history.push('/'))
|
||||||
.catch(e => console.error(e))
|
.catch(e => console.error(e))
|
||||||
;
|
;
|
||||||
}
|
}
|
||||||
}>
|
}>
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="email">Email:</label>
|
<label htmlFor='email'>Email:</label>
|
||||||
<input type="email" name="email" ref={e => emailElement = e} />
|
<input type='email' name='email' ref={e => emailElement = e} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="username">Username:</label>
|
<label htmlFor='username'>Username:</label>
|
||||||
<input type="text" name="username" ref={e => usernameElement = e} />
|
<input type='text' name='username' ref={e => usernameElement = e} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="password">Password:</label>
|
<label htmlFor='password'>Password:</label>
|
||||||
<input type="password" name="password" ref={e => passwordElement = e} />
|
<input type='password' name='password' ref={e => passwordElement = e} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="retype">Retype Password:</label>
|
<label htmlFor='retype'>Retype Password:</label>
|
||||||
<input type="password" name="retype" ref={e => retypeElement = e} />
|
<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>
|
</div>
|
||||||
|
|
||||||
<button type='submit'>Signup</button>
|
<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();
|
email = email.trim();
|
||||||
username = username.trim();
|
username = username.trim();
|
||||||
|
|
||||||
const err = handleValidation(email, username, password, retype);
|
const err = handleValidation(email, username, password, retype);
|
||||||
|
|
||||||
if (err) {
|
if (err) {
|
||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
@@ -73,6 +79,7 @@ const handleSubmit = async (email, username, password, retype) => {
|
|||||||
formData.append('email', email);
|
formData.append('email', email);
|
||||||
formData.append('username', username);
|
formData.append('username', username);
|
||||||
formData.append('password', password);
|
formData.append('password', password);
|
||||||
|
formData.append('contact', contact)
|
||||||
|
|
||||||
const result = await fetch('/api/accounts/signup', { method: 'POST', body: formData });
|
const result = await fetch('/api/accounts/signup', { method: 'POST', body: formData });
|
||||||
|
|
||||||
|
|||||||
@@ -5,13 +5,13 @@ const DeleteAccount = props => {
|
|||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
if (!open) {
|
if (!open) {
|
||||||
return <button onClick={() => setOpen(true)}>Delete Account</button>
|
return <button onClick={() => setOpen(true)} className={props.className}>Delete Account</button>
|
||||||
}
|
}
|
||||||
|
|
||||||
let passwordElement;
|
let passwordElement;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form className='constricted' onSubmit={async evt => {
|
<form className={props.className} onSubmit={async evt => {
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
const password = passwordElement.value;
|
const password = passwordElement.value;
|
||||||
passwordElement.value = '';
|
passwordElement.value = '';
|
||||||
@@ -34,7 +34,7 @@ const handleSubmit = async (password) => {
|
|||||||
|
|
||||||
formData.append('password', 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) {
|
if (!result.ok) {
|
||||||
alert(await result.text());
|
alert(await result.text());
|
||||||
|
|||||||
+3
-1
@@ -25,6 +25,7 @@ const question = (prompt, def) => {
|
|||||||
const projectMailSMTP = await question('Project Mail SMTP', 'smtp.example.com');
|
const projectMailSMTP = await question('Project Mail SMTP', 'smtp.example.com');
|
||||||
const projectMailUser = await question('Project Mail Username', 'foobar@example.com');
|
const projectMailUser = await question('Project Mail Username', 'foobar@example.com');
|
||||||
const projectMailPass = await question('Project Mail Password', 'foobar');
|
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 projectDBUser = await question('Project Database Username', projectName);
|
||||||
const projectDBPass = await question('Project Database Password', 'pikachu');
|
const projectDBPass = await question('Project Database Password', 'pikachu');
|
||||||
|
|
||||||
@@ -64,7 +65,8 @@ services:
|
|||||||
- WEB_PORT=3000
|
- WEB_PORT=3000
|
||||||
- MAIL_SMTP=${projectMailSMTP}
|
- MAIL_SMTP=${projectMailSMTP}
|
||||||
- MAIL_USERNAME=${projectMailUser}
|
- MAIL_USERNAME=${projectMailUser}
|
||||||
- MAIL_PASSWORD=${projectMailPass}
|
- MAIL_PASSWORD=${projectMailPass}
|
||||||
|
- MAIL_PHYSICAL=${projectMailPhysical}
|
||||||
- DB_HOSTNAME=database
|
- DB_HOSTNAME=database
|
||||||
- DB_DATABASE=${projectName}
|
- DB_DATABASE=${projectName}
|
||||||
- DB_USERNAME=${projectDBUser}
|
- DB_USERNAME=${projectDBUser}
|
||||||
|
|||||||
Generated
+658
-31
File diff suppressed because it is too large
Load Diff
@@ -26,6 +26,7 @@
|
|||||||
"homepage": "https://github.com/KRGameStudios/MERN-template#readme",
|
"homepage": "https://github.com/KRGameStudios/MERN-template#readme",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
|
"connect-session-sequelize": "^7.1.0",
|
||||||
"cookie-parser": "^1.4.5",
|
"cookie-parser": "^1.4.5",
|
||||||
"core-js": "^3.8.3",
|
"core-js": "^3.8.3",
|
||||||
"dateformat": "^4.5.1",
|
"dateformat": "^4.5.1",
|
||||||
|
|||||||
@@ -2,10 +2,16 @@ const express = require('express');
|
|||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
//basic account management
|
//basic account management
|
||||||
|
router.get('/', require('./query'));
|
||||||
|
router.patch('/', require('./update'));
|
||||||
|
|
||||||
|
//signup -> login -> logout
|
||||||
router.post('/signup', require('./signup'));
|
router.post('/signup', require('./signup'));
|
||||||
router.get('/validation', require('./validation'));
|
router.get('/validation', require('./validation'));
|
||||||
router.post('/login', require('./login'));
|
router.post('/login', require('./login'));
|
||||||
router.post('/logout', require('./logout'));
|
router.post('/logout', require('./logout'));
|
||||||
router.post('/deletion', require('./deletion'));
|
|
||||||
|
//account deletion
|
||||||
|
router.delete('/deletion', require('./deletion'));
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ const route = async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//save the session and cookie data
|
//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);
|
res.cookie('loggedin', process.env.WEB_ADDRESS);
|
||||||
|
|
||||||
if (account.privilege == 'administrator') {
|
if (account.privilege == 'administrator') {
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -101,6 +101,7 @@ const registerPendingSignup = async (fields, hash, token) => {
|
|||||||
email: fields.email,
|
email: fields.email,
|
||||||
username: fields.username,
|
username: fields.username,
|
||||||
hash: hash,
|
hash: hash,
|
||||||
|
contact: fields.contact,
|
||||||
token: token
|
token: token
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -109,7 +110,12 @@ const registerPendingSignup = async (fields, hash, token) => {
|
|||||||
|
|
||||||
const sendValidationEmail = async (email, username, 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 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;
|
let transporter, info;
|
||||||
|
|
||||||
@@ -143,7 +149,7 @@ const sendValidationEmail = async (email, username, token) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (info.accepted[0] != email) {
|
if (info.accepted[0] != email) {
|
||||||
return 'validation email failed';
|
return 'validation email failed to send';
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -29,7 +29,8 @@ const route = async (req, res) => {
|
|||||||
accounts.create({
|
accounts.create({
|
||||||
email: info.email,
|
email: info.email,
|
||||||
username: info.username,
|
username: info.username,
|
||||||
hash: info.hash
|
hash: info.hash,
|
||||||
|
contact: info.contact
|
||||||
});
|
});
|
||||||
|
|
||||||
//finally
|
//finally
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -16,27 +16,4 @@ router.get('/banned', require('./banned'));
|
|||||||
router.post('/ban', require('./ban'));
|
router.post('/ban', require('./ban'));
|
||||||
router.post('/unban', require('./unban'));
|
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;
|
module.exports = router;
|
||||||
@@ -28,6 +28,12 @@ module.exports = sequelize.define('accounts', {
|
|||||||
|
|
||||||
hash: 'varchar(100)', //for passwords
|
hash: 'varchar(100)', //for passwords
|
||||||
|
|
||||||
|
contact: {
|
||||||
|
type: Sequelize.BOOLEAN,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: false
|
||||||
|
},
|
||||||
|
|
||||||
deletion: {
|
deletion: {
|
||||||
type: 'DATETIME',
|
type: 'DATETIME',
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
|
|||||||
@@ -14,5 +14,11 @@ module.exports = sequelize.define('pendingSignups', {
|
|||||||
|
|
||||||
hash: 'varchar(100)', //for passwords
|
hash: 'varchar(100)', //for passwords
|
||||||
|
|
||||||
|
contact: {
|
||||||
|
type: Sequelize.BOOLEAN,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: false
|
||||||
|
},
|
||||||
|
|
||||||
token: Sequelize.INTEGER(11)
|
token: Sequelize.INTEGER(11)
|
||||||
});
|
});
|
||||||
|
|||||||
+14
-4
@@ -11,19 +11,29 @@ const path = require('path');
|
|||||||
const formidable = require('express-formidable');
|
const formidable = require('express-formidable');
|
||||||
const cookieParser = require('cookie-parser');
|
const cookieParser = require('cookie-parser');
|
||||||
const session = require('express-session');
|
const session = require('express-session');
|
||||||
|
const SequelizeStore = require("connect-session-sequelize")(session.Store);
|
||||||
app.use(formidable());
|
|
||||||
app.use(cookieParser());
|
|
||||||
app.use(session({ secret: process.env.SESSION_SECRET, resave: true, saveUninitialized: true }));
|
|
||||||
|
|
||||||
//database connection
|
//database connection
|
||||||
const database = require('./database');
|
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
|
//account management
|
||||||
app.use('/api/accounts', require('./accounts'));
|
app.use('/api/accounts', require('./accounts'));
|
||||||
|
|
||||||
//administration
|
//administration
|
||||||
app.use('/api/admin', require('./admin'));
|
app.use('/api/admin', require('./admin'));
|
||||||
|
require('./admin/bookkeeper')(); //BUGFIX
|
||||||
|
|
||||||
//send static files
|
//send static files
|
||||||
app.use('/', express.static(path.resolve(__dirname, '..', 'public')));
|
app.use('/', express.static(path.resolve(__dirname, '..', 'public')));
|
||||||
|
|||||||
Reference in New Issue
Block a user