diff --git a/controllers/authController.js b/controllers/authController.js index 000a193..cece374 100644 --- a/controllers/authController.js +++ b/controllers/authController.js @@ -6,7 +6,6 @@ const perms = require('./permissionController') const tokens = require('./tokenController') const utils = require('./utilsController') const ClientError = require('./utils/ClientError') -const ServerError = require('./utils/ServerError') const config = require('./../config') // Don't forget to update min/max length of text inputs in auth.njk @@ -96,10 +95,7 @@ self.register = async (req, res) => { const hash = await bcrypt.hash(password, saltRounds) - const token = await tokens.generateUniqueToken() - if (!token) { - throw new ServerError('Failed to allocate a unique token. Try again?') - } + const token = await tokens.getUniqueToken(res) await utils.db.table('users') .insert({ @@ -110,8 +106,8 @@ self.register = async (req, res) => { permission: perms.permissions.user, registration: Math.floor(Date.now() / 1000) }) + utils.invalidateStatsCache('users') - tokens.onHold.delete(token) return res.json({ success: true, token }) } @@ -195,10 +191,7 @@ self.createUser = async (req, res) => { const hash = await bcrypt.hash(password, saltRounds) - const token = await tokens.generateUniqueToken() - if (!token) { - throw new ServerError('Failed to allocate a unique token. Try again?') - } + const token = await tokens.getUniqueToken(res) await utils.db.table('users') .insert({ @@ -209,8 +202,8 @@ self.createUser = async (req, res) => { permission, registration: Math.floor(Date.now() / 1000) }) + utils.invalidateStatsCache('users') - tokens.onHold.delete(token) return res.json({ success: true, username, password, group }) } diff --git a/controllers/tokenController.js b/controllers/tokenController.js index e82c667..5fb5b46 100644 --- a/controllers/tokenController.js +++ b/controllers/tokenController.js @@ -3,17 +3,23 @@ const perms = require('./permissionController') const utils = require('./utilsController') const ClientError = require('./utils/ClientError') const ServerError = require('./utils/ServerError') +const logger = require('./../logger') const self = { tokenLength: 64, tokenMaxTries: 3, - onHold: new Set() + + onHold: new Set() // temporarily held random tokens } -self.generateUniqueToken = async () => { +self.getUniqueToken = async res => { for (let i = 0; i < self.tokenMaxTries; i++) { const token = randomstring.generate(self.tokenLength) - if (self.onHold.has(token)) continue + + if (self.onHold.has(token)) { + logger.debug(`Token ${utils.mask(token)} is currently held by another request (${i + 1}/${utils.idMaxTries}).`) + continue + } // Put token on-hold (wait for it to be inserted to DB) self.onHold.add(token) @@ -24,13 +30,36 @@ self.generateUniqueToken = async () => { .first() if (user) { self.onHold.delete(token) + logger.debug(`User with token ${utils.mask(token)} already exists (${i + 1}/${utils.idMaxTries}).`) continue } + // Unhold token once the Response has been sent + if (res) { + // Keep in an array for future-proofing + // if a single Request needs to generate multiple tokens + if (!res.locals.tokens) { + res.locals.tokens = [] + res.once('finish', () => { self.unholdTokens(res) }) + } + res.locals.tokens.push(token) + } + return token } - return null + throw new ServerError('Failed to allocate a unique token. Try again?') +} + +self.unholdTokens = res => { + if (!res.locals.tokens) return + + for (const token of res.locals.tokens) { + self.onHold.delete(token) + logger.debug(`Unheld token ${utils.mask(token)}.`) + } + + delete res.locals.tokens } self.verify = async (req, res) => { @@ -84,10 +113,7 @@ self.list = async (req, res) => { self.change = async (req, res) => { const user = await utils.authorize(req, 'token') - const newToken = await self.generateUniqueToken() - if (!newToken) { - throw new ServerError('Failed to allocate a unique token. Try again?') - } + const newToken = await self.getUniqueToken(res) await utils.db.table('users') .where('token', user.token) @@ -95,7 +121,6 @@ self.change = async (req, res) => { token: newToken, timestamp: Math.floor(Date.now() / 1000) }) - self.onHold.delete(newToken) return res.json({ success: true, token: newToken }) } diff --git a/controllers/utilsController.js b/controllers/utilsController.js index b741ae9..6cecb2f 100644 --- a/controllers/utilsController.js +++ b/controllers/utilsController.js @@ -362,7 +362,7 @@ self.escape = string => { } self.stripIndents = string => { - if (!string) return + if (!string) return string const result = string.replace(/^[^\S\n]+/gm, '') const match = result.match(/^[^\S\n]*(?=\S)/gm) const indent = match && Math.min(...match.map(el => el.length)) @@ -373,6 +373,19 @@ self.stripIndents = string => { return result } +self.mask = string => { + if (!string) return string + const max = Math.min(Math.floor(string.length / 2), 8) + const fragment = Math.floor(max / 2) + if (string.length <= fragment) { + return '*'.repeat(string.length) + } else { + return string.substring(0, fragment) + + '*'.repeat(Math.min(string.length - (fragment * 2), 4)) + + string.substring(string.length - fragment) + } +} + self.assertRequestType = (req, type) => { if (!req.is(type)) { throw new ClientError(`Request Content-Type must be ${type}.`)