refactor: generateUniqueToken -> getUniqueToken

this now matches lifecycle with similar functions in upload and album
controllers

also added a new util function .mask() for basic string masking
This commit is contained in:
Bobby 2022-07-30 08:37:57 +07:00
parent b7dcf30578
commit 4591b8bb42
No known key found for this signature in database
GPG Key ID: 941839794CBF5A09
3 changed files with 52 additions and 21 deletions

View File

@ -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 })
}

View File

@ -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 })
}

View File

@ -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}.`)