Stripped out a whole bunch of pages, read more

The purpose of this branch is to bring this project in line with the JWT
protcol that the microservice is using. For the time being, it's easier
to get a stripped-down and stable build and replace the lost parts, one-
by-one.
This commit is contained in:
2021-03-08 12:34:41 +11:00
parent e3e5af4af0
commit 7c09ac46da
46 changed files with 310 additions and 4150 deletions
-51
View File
@@ -1,51 +0,0 @@
//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 * * * *', () => {
accounts.destroy({
where: {
deletion: {
[Op.lt]: Sequelize.fn('NOW')
}
}
});
});
module.exports = route;
-17
View File
@@ -1,17 +0,0 @@
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'));
//account deletion
router.delete('/deletion', require('./deletion'));
module.exports = router;
-86
View File
@@ -1,86 +0,0 @@
//libraries
const utils = require('util');
const bcrypt = require('bcryptjs');
const Sequelize = require('sequelize');
const Op = Sequelize.Op;
const { bannedEmails, accounts } = require('../database/models');
//utilities
const validateEmail = require('../../common/utilities/validate-email.js');
//api/accounts/login
const route = async (req, res) => {
//validate the given details
const validateErr = await validateDetails(req.fields);
if (validateErr) {
return res.status(401).send(validateErr);
}
//get the existing account
const account = await accounts.findOne({
where: {
email: req.fields.email
}
});
if (!account) {
return res.status(401).send('incorrect email or password');
}
//compare passwords
const compare = utils.promisify(bcrypt.compare);
const match = await compare(req.fields.password, account.hash);
if (!match) {
return res.status(401).send('incorrect email or password');
}
//save the session and cookie data
req.session.account = JSON.parse(JSON.stringify(account.dataValues));
res.cookie('loggedin', process.env.WEB_ADDRESS);
if (account.privilege == 'administrator') {
res.cookie('admin', process.env.SESSION_ADMIN);
}
//cancel deletion if any
await accounts.update({ deletion: null }, {
where: {
id: account.id
}
});
//finally
res.status(200).send('login succeeded');
};
const validateDetails = async (fields) => {
//basic formatting (with an exception for the default admin account)
if (!validateEmail(fields.email) && fields.email != `admin@${process.env.WEB_ADDRESS}`) {
return 'invalid email';
}
//check for existing (banned)
const banned = await bannedEmails.findAll({
where: {
[Op.and]: {
email: fields.email,
expiry: {
[Op.or]: {
[Op.gt]: Sequelize.fn('NOW'),
[Op.eq]: null
}
}
}
}
});
if (banned.length > 0) {
return 'banned email';
}
return null;
}
module.exports = route;
-11
View File
@@ -1,11 +0,0 @@
const route = (req, res) => {
//clear cookies and stored data
req.session.account = null;
res.clearCookie('loggedin');
res.clearCookie('admin');
res.clearCookie('pseudonym');
return res.status(200).end();
};
module.exports = route;
-21
View File
@@ -1,21 +0,0 @@
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;
-159
View File
@@ -1,159 +0,0 @@
//libraries
const bcrypt = require('bcryptjs');
const nodemailer = require('nodemailer');
const Sequelize = require('sequelize');
const Op = Sequelize.Op;
const { bannedEmails, accounts, pendingSignups } = require('../database/models');
//utilities
const validateEmail = require('../../common/utilities/validate-email.js');
const validateUsername = require('../../common/utilities/validate-username.js');
//api/accounts/signup
const route = async (req, res) => {
//validate the given details
const validateErr = await validateDetails(req.fields);
if (validateErr) {
return res.status(401).send(validateErr);
}
//generate the password hash
const salt = await bcrypt.genSalt(11);
const hash = await bcrypt.hash(req.fields.password, salt);
//generate the validation field
const token = Math.floor(Math.random() * 2000000000);
//register signup
const signupErr = await registerPendingSignup(req.fields, hash, token);
if (signupErr) {
return res.status(500).send(signupErr);
}
//send the validation email
const emailErr = await sendValidationEmail(req.fields.email, req.fields.username, token);
if (emailErr) {
return res.status(500).send(emailErr);
}
//finally
res.status(200).send("Validation email sent!");
return null;
}
const validateDetails = async (fields) => {
//basic formatting
if (!validateEmail(fields.email)) {
return 'invalid email';
}
if (!validateUsername(fields.username)) {
return 'invalid username';
}
//check for existing (banned)
const banned = await bannedEmails.findAll({
where: {
[Op.and]: {
email: fields.email,
expiry: {
[Op.or]: {
[Op.gt]: Sequelize.fn('NOW'),
[Op.eq]: null
}
}
}
}
});
if (banned.length > 0) {
return 'banned email';
}
//check for existing email
const email = await accounts.findOne({
where: {
email: fields.email
}
});
if (email) {
return 'email already exists';
}
//check for existing username
const username = await accounts.findOne({
where: {
username: fields.username
}
});
if (username) {
return 'username already exists';
}
return null;
};
const registerPendingSignup = async (fields, hash, token) => {
const record = await pendingSignups.upsert({
email: fields.email,
username: fields.username,
hash: hash,
contact: fields.contact,
token: token
});
return null;
};
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 ${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;
//what exactly is a transport?
try {
transporter = nodemailer.createTransport({
host: process.env.MAIL_SMTP,
port: 465,
secure: true,
auth: {
user: process.env.MAIL_USERNAME,
pass: process.env.MAIL_PASSWORD
},
});
}
catch(e) {
return `failed to create transport: ${e}`;
}
// send mail with defined transport object
try {
info = await transporter.sendMail({
from: `signup@${process.env.WEB_ADDRESS}`, //WARNING: google overwrites this
to: email,
subject: 'Email Validation',
text: msg
});
}
catch(e) {
return `failed to send mail ${e}`;
}
if (info.accepted[0] != email) {
return 'validation email failed to send';
}
return null;
};
module.exports = route;
-34
View File
@@ -1,34 +0,0 @@
const bcrypt = require('bcryptjs');
const { accounts } = require('../database/models');
const route = async (req, res) => {
if (!req.session.account.id) {
return res.status(500).send('missing account data');
}
//generate the password hash
const salt = await bcrypt.genSalt(11);
const hash = await bcrypt.hash(req.fields.password, salt);
//update the account
await accounts.update({
contact: req.fields.contact,
hash: hash
}, {
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;
-40
View File
@@ -1,40 +0,0 @@
const { pendingSignups, accounts } = require('../database/models');
//api/accounts/validation
const route = async (req, res) => {
//get the existing pending signup
const info = await pendingSignups.findOne({
where: {
username: req.query.username
}
});
//check the given info
if (!info) {
return res.status(401).send('validation failed');
}
if (info.token != req.query.token) {
return res.status(401).send('tokens do not match');
}
//delete the pending signup
pendingSignups.destroy({
where: {
username: req.query.username
}
});
//move data to the accounts table
accounts.create({
email: info.email,
username: info.username,
hash: info.hash,
contact: info.contact
});
//finally
res.status(200).send('Validation succeeded!');
};
module.exports = route;
-40
View File
@@ -1,40 +0,0 @@
const { Op } = require('sequelize');
const { bannedEmails, accounts } = require('../database/models');
const route = async (req, res) => {
//fetch the account based on the email or username
const account = await accounts.findOne({
attrubutes: ['username', 'email'],
where: {
[Op.or]: {
username: {
[Op.eq]: req.fields.username,
},
email: {
[Op.eq]: req.fields.email
}
}
}
});
//just in case
if (account && account.privilege == 'administrator') {
return res.status(401).send('Couldn\'t ban an admin');
}
//need either an email or an account
if (!account && !req.fields.email) {
return res.status(401).send('Couldn\'t determine the ban info');
}
//apply the ban
await bannedEmails.upsert({
email: (account || req.fields).email,
reason: req.fields.reason ? req.fields.reason : null,
expiry: req.fields.expiry ? new Date(Date.parse(req.fields.expiry)) : null
});
return res.status(200).send(`Email ${(account || req.fields).email} banned (username ${account ? account.username : 'not found'})`);
};
module.exports = route;
-34
View File
@@ -1,34 +0,0 @@
const { Op } = require('sequelize');
const { bannedEmails, accounts } = require('../database/models');
const route = async (req, res) => {
//merge the banned accounts with the account data, if any
const data = await bannedEmails.findAll()
.then(bans => bans.map(async ban => {
//find a matching account
const account = await accounts.findOne({
attrubutes: ['username', 'privilege'],
where: {
email: {
[Op.eq]: ban.email
}
}
}) || {};
//merge the data and return (becomes a promise)
return {
username: account.username,
email: ban.email,
privilege: account.privilege,
expiry: ban.expiry,
reason: ban.reason
};
}))
.then(promises => Promise.all(promises)) //resolve promises
.catch(e => console.error(e))
;
return res.status(200).json(data);
};
module.exports = route;
-29
View File
@@ -1,29 +0,0 @@
//DOCS: this whole file is just a big bugfix
//DOCS: ensure that there is at least one administration account
const bcrypt = require('bcryptjs');
const sequelize = require('../database');
const { accounts } = require('../database/models');
const defaultAdminAccount = async () => {
await sequelize.sync(); //this whole file is just one big BUGFIX
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))
});
//TODO: (1) Replace this default admin account password with UUID
console.log(`Created default admin account (email: admin@${process.env.WEB_ADDRESS}; password: password)`);
}
};
module.exports = defaultAdminAccount;
-19
View File
@@ -1,19 +0,0 @@
const express = require('express');
const router = express.Router();
//middleware
router.use((req, res, next) => {
//make sure the account is an admin
if (req.cookies['admin'] !== process.env.SESSION_ADMIN) { //TODO: Eew not good.
return res.status(401).send('invalid admin status');
} else {
next();
}
});
//basic account ban management
router.get('/banned', require('./banned'));
router.post('/ban', require('./ban'));
router.post('/unban', require('./unban'));
module.exports = router;
-46
View File
@@ -1,46 +0,0 @@
const Sequelize = require('sequelize');
const Op = Sequelize.Op;
const { bannedEmails, accounts } = require('../database/models');
var cron = require('node-cron');
const route = async (req, res) => {
console.log(req.fields.entry)
//get the account, if one is found
const account = await accounts.findOne({
where: {
[Op.or]: {
email: {
[Op.eq]: req.fields.entry
},
username: {
[Op.eq]: req.fields.entry
}
}
},
});
//accept either email or username
const affectedRows = await bannedEmails.destroy({
where: {
email: {
[Op.eq]: account?.email || req.fields.entry || ''
}
}
});
return res.status(200).send(`${affectedRows} emails unbanned`);
};
//delete any expired bans
cron.schedule('0 * * * *', () => {
bannedEmails.destroy({
where: {
expiry: {
[Op.lt]: Sequelize.fn('NOW'),
[Op.not]: null
}
}
});
});
module.exports = route;
-7
View File
@@ -1,7 +0,0 @@
const express = require('express');
const router = express.Router();
//reserve the name on the chat server (then get out of the way)
router.post('/reserve', require('./reserve'));
module.exports = router;
-32
View File
@@ -1,32 +0,0 @@
const fetch = require('node-fetch');
const FormData = require('form-data');
const route = async (req, res) => {
if (!req.session.account) {
return status(403).send('No account detected');
}
//build the fake form data object
let form = new FormData();
form.append('username', req.session?.account?.username);
form.append('key', process.env.CHAT_KEY);
try {
//reserve the UUID with the chat server (hop 1)
const result = await fetch(`http${process.env.PRODUCTION ? 's' : ''}://${process.env.CHAT_URI}/reserve`, { method: 'POST', body: form });
if (result.status == 200) {
const json = await result.json();
res.cookie('pseudonym', json.pseudonym);
res.status(200).send({ ok: true });
} else {
throw await result.text();
}
} catch(e) {
console.error(`Chat server error: ${e}`);
res.cookie('pseudonym', '.null');
res.status(200).send({ ok: false, error: `Chat server error ${e}` });
}
};
module.exports = route;
+1 -1
View File
@@ -4,7 +4,7 @@ const sequelize = new Sequelize(process.env.DB_DATABASE, process.env.DB_USERNAME
host: process.env.DB_HOSTNAME,
dialect: 'mariadb',
timezone: process.env.DB_TIMEZONE,
// logging: false
logging: false
});
sequelize.sync();
-42
View File
@@ -1,42 +0,0 @@
const Sequelize = require('sequelize');
const sequelize = require('..');
module.exports = sequelize.define('accounts', {
id: {
type: Sequelize.INTEGER(11),
allowNull: false,
autoIncrement: true,
primaryKey: true,
unique: true
},
privilege: {
type: Sequelize.ENUM,
values: ['administrator', 'moderator', 'alpha', 'beta', 'gamma', 'normal'],
defaultValue: 'normal'
},
email: {
type: 'varchar(320)',
unique: true
},
username: {
type: 'varchar(320)',
unique: true
},
hash: 'varchar(100)', //for passwords
contact: {
type: Sequelize.BOOLEAN,
allowNull: false,
defaultValue: false
},
deletion: {
type: 'DATETIME',
allowNull: true,
defaultValue: null
}
});
-25
View File
@@ -1,25 +0,0 @@
const Sequelize = require('sequelize');
const sequelize = require('..');
module.exports = sequelize.define('bannedEmails', {
id: {
type: Sequelize.INTEGER(11),
allowNull: false,
autoIncrement: true,
primaryKey: true,
unique: true
},
email: {
type: 'varchar(320)',
unique: true
},
reason: Sequelize.TEXT,
expiry: {
type: 'DATETIME',
allowNull: true,
defaultValue: null
}
});
+1 -3
View File
@@ -1,5 +1,3 @@
module.exports = {
bannedEmails: require('./banned-emails'),
accounts: require('./accounts'),
pendingSignups: require('./pending-signups')
//TODO: models
}
-24
View File
@@ -1,24 +0,0 @@
const Sequelize = require('sequelize');
const sequelize = require('..');
module.exports = sequelize.define('pendingSignups', {
email: {
type: 'varchar(320)',
unique: true
},
username: {
type: 'varchar(320)',
unique: true
},
hash: 'varchar(100)', //for passwords
contact: {
type: Sequelize.BOOLEAN,
allowNull: false,
defaultValue: false
},
token: Sequelize.INTEGER(11)
});
+6 -31
View File
@@ -1,46 +1,21 @@
//environment variables
require('dotenv').config();
//libraries
const path = require('path');
//create the server
const express = require('express');
const app = express();
const server = require('http').Server(app);
const bodyParser = require('body-parser');
//libraries used here
const path = require('path');
const formidable = require('express-formidable');
const cookieParser = require('cookie-parser');
const session = require('express-session');
const SequelizeStore = require("connect-session-sequelize")(session.Store);
//config
app.use(bodyParser.json());
//database connection
const database = require('./database');
//setup the app middleware
app.use(formidable());
app.use(cookieParser());
app.use(session({
secret: process.env.SESSION_SECRET,
resave: true,
saveUninitialized: true,
store: new SequelizeStore({
db: database
})
}));
//invoke all models
const models = require('./database/models');
//account management
app.use('/api/accounts', require('./accounts'));
//chat management
app.use('/api/chat', require('./chat'));
//administration
app.use('/api/admin', require('./admin'));
require('./admin/bookkeeper')(); //BUGFIX
//send static files
app.use('/', express.static(path.resolve(__dirname, '..', 'public')));