diff --git a/packages/backend/src/CoreModule.js b/packages/backend/src/CoreModule.js index be2b392a..eece9209 100644 --- a/packages/backend/src/CoreModule.js +++ b/packages/backend/src/CoreModule.js @@ -210,6 +210,9 @@ const install = async ({ services, app }) => { const { AntiCSRFService } = require('./services/auth/AntiCSRFService'); services.registerService('anti-csrf', AntiCSRFService); + + const { LockService } = require('./services/LockService'); + services.registerService('lock', LockService); } const install_legacy = async ({ services }) => { diff --git a/packages/backend/src/routers/save_account.js b/packages/backend/src/routers/save_account.js index df076bd9..bb33b5fd 100644 --- a/packages/backend/src/routers/save_account.js +++ b/packages/backend/src/routers/save_account.js @@ -75,123 +75,132 @@ router.post('/save_account', auth, express.json(), async (req, res, next)=>{ return res.status(429).send('Too many requests.'); } - // duplicate username check, do this only if user has supplied a new username - if(req.body.username !== req.user.username && await username_exists(req.body.username)) - return res.status(400).send('This username already exists in our database. Please use another one.'); - // duplicate email check (pseudo-users don't count) - let rows2 = await db.read(`SELECT EXISTS(SELECT 1 FROM user WHERE email=? AND password IS NOT NULL) AS email_exists`, [req.body.email]); - if(rows2[0].email_exists) - return res.status(400).send('This email already exists in our database. Please use another one.'); - // get pseudo user, if exists - let pseudo_user = await db.read(`SELECT * FROM user WHERE email = ? AND password IS NULL`, [req.body.email]); - pseudo_user = pseudo_user[0]; + const svc_lock = req.services.get('lock'); + return svc_lock.lock([ + `save-account:username:${req.body.username}`, + `save-account:email:${req.body.email}` + ], async () => { + await new Promise((rslv) => { + setTimeout(rslv, 5000); + }); + // duplicate username check, do this only if user has supplied a new username + if(req.body.username !== req.user.username && await username_exists(req.body.username)) + return res.status(400).send('This username already exists in our database. Please use another one.'); + // duplicate email check (pseudo-users don't count) + let rows2 = await db.read(`SELECT EXISTS(SELECT 1 FROM user WHERE email=? AND password IS NOT NULL) AS email_exists`, [req.body.email]); + if(rows2[0].email_exists) + return res.status(400).send('This email already exists in our database. Please use another one.'); + // get pseudo user, if exists + let pseudo_user = await db.read(`SELECT * FROM user WHERE email = ? AND password IS NULL`, [req.body.email]); + pseudo_user = pseudo_user[0]; - // send_confirmation_code - req.body.send_confirmation_code = req.body.send_confirmation_code ?? true; + // send_confirmation_code + req.body.send_confirmation_code = req.body.send_confirmation_code ?? true; - // todo email confirmation is required by default unless: - // Pseudo user converting and matching uuid is provided - let email_confirmation_required = 0; + // todo email confirmation is required by default unless: + // Pseudo user converting and matching uuid is provided + let email_confirmation_required = 0; - // ----------------------------------- - // Get referral user - // ----------------------------------- - let referred_by_user = undefined; - if ( req.body.referral_code ) { - referred_by_user = await get_user({ referral_code: req.body.referral_code }); - if ( ! referred_by_user ) { - return res.status(400).send('Referral code not found'); + // ----------------------------------- + // Get referral user + // ----------------------------------- + let referred_by_user = undefined; + if ( req.body.referral_code ) { + referred_by_user = await get_user({ referral_code: req.body.referral_code }); + if ( ! referred_by_user ) { + return res.status(400).send('Referral code not found'); + } } - } - // ----------------------------------- - // New User - // ----------------------------------- - const user_uuid = req.user.uuid; - let email_confirm_code = Math.floor(100000 + Math.random() * 900000); - const email_confirm_token = uuidv4(); + // ----------------------------------- + // New User + // ----------------------------------- + const user_uuid = req.user.uuid; + let email_confirm_code = Math.floor(100000 + Math.random() * 900000); + const email_confirm_token = uuidv4(); - if(pseudo_user === undefined){ - await db.write( - `UPDATE user - SET - username = ?, email = ?, password = ?, email_confirm_code = ?, email_confirm_token = ?${ - referred_by_user ? ', referred_by = ?' : '' } - WHERE - id = ?`, - [ - // username - req.body.username, - // email - req.body.email, - // password - await bcrypt.hash(req.body.password, 8), - // email_confirm_code - email_confirm_code, - //email_confirm_token - email_confirm_token, - // referred_by - ...(referred_by_user ? [referred_by_user.id] : []), - // id - req.user.id - ] - ); - invalidate_cached_user(req.user); + if(pseudo_user === undefined){ + await db.write( + `UPDATE user + SET + username = ?, email = ?, password = ?, email_confirm_code = ?, email_confirm_token = ?${ + referred_by_user ? ', referred_by = ?' : '' } + WHERE + id = ?`, + [ + // username + req.body.username, + // email + req.body.email, + // password + await bcrypt.hash(req.body.password, 8), + // email_confirm_code + email_confirm_code, + //email_confirm_token + email_confirm_token, + // referred_by + ...(referred_by_user ? [referred_by_user.id] : []), + // id + req.user.id + ] + ); + invalidate_cached_user(req.user); - // Update root directory name - await db.write( - `UPDATE fsentries SET name = ? WHERE user_id = ? and parent_uid IS NULL`, - [ - // name - req.body.username, - // id - req.user.id, - ] - ); - const filesystem = req.services.get('filesystem'); - await filesystem.update_child_paths(`/${req.user.username}`, `/${req.body.username}`, req.user.id); + // Update root directory name + await db.write( + `UPDATE fsentries SET name = ? WHERE user_id = ? and parent_uid IS NULL`, + [ + // name + req.body.username, + // id + req.user.id, + ] + ); + const filesystem = req.services.get('filesystem'); + await filesystem.update_child_paths(`/${req.user.username}`, `/${req.body.username}`, req.user.id); - if(req.body.send_confirmation_code) - send_email_verification_code(email_confirm_code, req.body.email); - else - send_email_verification_token(email_confirm_token, req.body.email, user_uuid); - } - - // create token for login - const svc_auth = req.services.get('auth'); - const { token } = await svc_auth.create_session_token(req.user, { req }); - - // user id - // todo if pseudo user, assign directly no need to do another DB lookup - const user_id = req.user.id; - const user_res = await db.read('SELECT * FROM `user` WHERE `id` = ? LIMIT 1', [user_id]); - const user = user_res[0]; - - // todo send LINK-based verification email - - //set cookie - res.cookie(config.cookie_name, token); - - { - const svc_event = req.services.get('event'); - svc_event.emit('user.save_account', { user }); - } - - // return results - return res.send({ - token: token, - user:{ - username: user.username, - uuid: user.uuid, - email: user.email, - is_temp: false, - requires_email_confirmation: user.requires_email_confirmation, - email_confirmed: user.email_confirmed, - email_confirmation_required: email_confirmation_required, - taskbar_items: await get_taskbar_items(user), - referral_code: user.referral_code, + if(req.body.send_confirmation_code) + send_email_verification_code(email_confirm_code, req.body.email); + else + send_email_verification_token(email_confirm_token, req.body.email, user_uuid); } - }) + + // create token for login + const svc_auth = req.services.get('auth'); + const { token } = await svc_auth.create_session_token(req.user, { req }); + + // user id + // todo if pseudo user, assign directly no need to do another DB lookup + const user_id = req.user.id; + const user_res = await db.read('SELECT * FROM `user` WHERE `id` = ? LIMIT 1', [user_id]); + const user = user_res[0]; + + // todo send LINK-based verification email + + //set cookie + res.cookie(config.cookie_name, token); + + { + const svc_event = req.services.get('event'); + svc_event.emit('user.save_account', { user }); + } + + // return results + return res.send({ + token: token, + user:{ + username: user.username, + uuid: user.uuid, + email: user.email, + is_temp: false, + requires_email_confirmation: user.requires_email_confirmation, + email_confirmed: user.email_confirmed, + email_confirmation_required: email_confirmation_required, + taskbar_items: await get_taskbar_items(user), + referral_code: user.referral_code, + } + }) + }); }) module.exports = router \ No newline at end of file diff --git a/packages/backend/src/services/LockService.js b/packages/backend/src/services/LockService.js new file mode 100644 index 00000000..25161ba0 --- /dev/null +++ b/packages/backend/src/services/LockService.js @@ -0,0 +1,105 @@ +const { RWLock } = require("../util/lockutil"); +const BaseService = require("./BaseService"); + +/** + * LockService implements robust critical sections when the behavior + * might return early or throw an error. + * + * This serivces uses RWLock but always locks in write mode. + */ +class LockService extends BaseService { + async _construct () { + this.locks = {}; + } + async _init () { + const svc_commands = this.services.get('commands'); + svc_commands.registerCommands('lock', [ + { + id: 'locks', + description: 'lists locks', + handler: async (args, log) => { + for ( const name in this.locks ) { + let line = name + ': '; + if ( this.locks[name].effective_mode === RWLock.TYPE_READ ) { + line += `READING (${this.locks[name].readers_})`; + log.log(line); + } + else + if ( this.locks[name].effective_mode === RWLock.TYPE_WRITE ) { + line += 'WRITING'; + log.log(line); + } + else { + line += 'UNKNOWN'; + log.log(line); + + // log the lock's internal state + const lines = JSON.stringify( + this.locks[name], + null, 2 + ).split('\n'); + for ( const line of lines ) { + log.log(' -> ' + line); + } + } + } + } + } + ]); + } + + async lock (name, opt_options, callback) { + if ( typeof opt_options === 'function' ) { + callback = opt_options; + opt_options = {}; + } + + // If name is an array, lock all of them + if ( Array.isArray(name) ) { + const names = name; + // TODO: verbose log option by service + // console.log('LOCKING NAMES', names) + const section = names.reduce((current_callback, name) => { + return async () => { + return await this.lock(name, opt_options, current_callback); + }; + }, callback); + + return await section(); + } + + if ( ! this.locks[name] ) { + const rwlock = new RWLock(); + this.locks[name] = rwlock; + } + + const handle = await this.locks[name].wlock(); + // TODO: verbose log option by service + // console.log(`\x1B[36;1mLOCK (${name})\x1B[0m`); + + + let timeout, timed_out; + if ( opt_options.timeout ) { + timeout = setTimeout(() => { + handle.unlock(); + // TODO: verbose log option by service + // throw new Error(`lock ${name} timed out`); + }, opt_options.timeout); + } + + try { + return await callback(); + } finally { + if ( timeout ) { + clearTimeout(timeout); + } + if ( ! timed_out ) { + // TODO: verbose log option by service + // console.log(`\x1B[36;1mUNLOCK (${name})\x1B[0m`); + handle.unlock(); + } + } + } +} + +module.exports = { LockService }; \ No newline at end of file diff --git a/packages/backend/src/services/fs/FSLockService.js b/packages/backend/src/services/fs/FSLockService.js index 83bf4988..a9fc3d0b 100644 --- a/packages/backend/src/services/fs/FSLockService.js +++ b/packages/backend/src/services/fs/FSLockService.js @@ -23,6 +23,7 @@ const BaseService = require("../BaseService"); const MODE_READ = Symbol('read'); const MODE_WRITE = Symbol('write'); +// TODO: DRY: could use LockService now class FSLockService extends BaseService { async _construct () { this.locks = {};