mirror of
https://github.com/HeyPuter/puter
synced 2024-11-15 06:15:47 +00:00
Add locking to save_account
This commit is contained in:
parent
b72e5b7e02
commit
691c8f1436
@ -210,6 +210,9 @@ const install = async ({ services, app }) => {
|
|||||||
|
|
||||||
const { AntiCSRFService } = require('./services/auth/AntiCSRFService');
|
const { AntiCSRFService } = require('./services/auth/AntiCSRFService');
|
||||||
services.registerService('anti-csrf', AntiCSRFService);
|
services.registerService('anti-csrf', AntiCSRFService);
|
||||||
|
|
||||||
|
const { LockService } = require('./services/LockService');
|
||||||
|
services.registerService('lock', LockService);
|
||||||
}
|
}
|
||||||
|
|
||||||
const install_legacy = async ({ services }) => {
|
const install_legacy = async ({ services }) => {
|
||||||
|
@ -75,123 +75,132 @@ router.post('/save_account', auth, express.json(), async (req, res, next)=>{
|
|||||||
return res.status(429).send('Too many requests.');
|
return res.status(429).send('Too many requests.');
|
||||||
}
|
}
|
||||||
|
|
||||||
// duplicate username check, do this only if user has supplied a new username
|
const svc_lock = req.services.get('lock');
|
||||||
if(req.body.username !== req.user.username && await username_exists(req.body.username))
|
return svc_lock.lock([
|
||||||
return res.status(400).send('This username already exists in our database. Please use another one.');
|
`save-account:username:${req.body.username}`,
|
||||||
// duplicate email check (pseudo-users don't count)
|
`save-account:email:${req.body.email}`
|
||||||
let rows2 = await db.read(`SELECT EXISTS(SELECT 1 FROM user WHERE email=? AND password IS NOT NULL) AS email_exists`, [req.body.email]);
|
], async () => {
|
||||||
if(rows2[0].email_exists)
|
await new Promise((rslv) => {
|
||||||
return res.status(400).send('This email already exists in our database. Please use another one.');
|
setTimeout(rslv, 5000);
|
||||||
// get pseudo user, if exists
|
});
|
||||||
let pseudo_user = await db.read(`SELECT * FROM user WHERE email = ? AND password IS NULL`, [req.body.email]);
|
// duplicate username check, do this only if user has supplied a new username
|
||||||
pseudo_user = pseudo_user[0];
|
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
|
// send_confirmation_code
|
||||||
req.body.send_confirmation_code = req.body.send_confirmation_code ?? true;
|
req.body.send_confirmation_code = req.body.send_confirmation_code ?? true;
|
||||||
|
|
||||||
// todo email confirmation is required by default unless:
|
// todo email confirmation is required by default unless:
|
||||||
// Pseudo user converting and matching uuid is provided
|
// Pseudo user converting and matching uuid is provided
|
||||||
let email_confirmation_required = 0;
|
let email_confirmation_required = 0;
|
||||||
|
|
||||||
// -----------------------------------
|
// -----------------------------------
|
||||||
// Get referral user
|
// Get referral user
|
||||||
// -----------------------------------
|
// -----------------------------------
|
||||||
let referred_by_user = undefined;
|
let referred_by_user = undefined;
|
||||||
if ( req.body.referral_code ) {
|
if ( req.body.referral_code ) {
|
||||||
referred_by_user = await get_user({ referral_code: req.body.referral_code });
|
referred_by_user = await get_user({ referral_code: req.body.referral_code });
|
||||||
if ( ! referred_by_user ) {
|
if ( ! referred_by_user ) {
|
||||||
return res.status(400).send('Referral code not found');
|
return res.status(400).send('Referral code not found');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// -----------------------------------
|
// -----------------------------------
|
||||||
// New User
|
// New User
|
||||||
// -----------------------------------
|
// -----------------------------------
|
||||||
const user_uuid = req.user.uuid;
|
const user_uuid = req.user.uuid;
|
||||||
let email_confirm_code = Math.floor(100000 + Math.random() * 900000);
|
let email_confirm_code = Math.floor(100000 + Math.random() * 900000);
|
||||||
const email_confirm_token = uuidv4();
|
const email_confirm_token = uuidv4();
|
||||||
|
|
||||||
if(pseudo_user === undefined){
|
if(pseudo_user === undefined){
|
||||||
await db.write(
|
await db.write(
|
||||||
`UPDATE user
|
`UPDATE user
|
||||||
SET
|
SET
|
||||||
username = ?, email = ?, password = ?, email_confirm_code = ?, email_confirm_token = ?${
|
username = ?, email = ?, password = ?, email_confirm_code = ?, email_confirm_token = ?${
|
||||||
referred_by_user ? ', referred_by = ?' : '' }
|
referred_by_user ? ', referred_by = ?' : '' }
|
||||||
WHERE
|
WHERE
|
||||||
id = ?`,
|
id = ?`,
|
||||||
[
|
[
|
||||||
// username
|
// username
|
||||||
req.body.username,
|
req.body.username,
|
||||||
// email
|
// email
|
||||||
req.body.email,
|
req.body.email,
|
||||||
// password
|
// password
|
||||||
await bcrypt.hash(req.body.password, 8),
|
await bcrypt.hash(req.body.password, 8),
|
||||||
// email_confirm_code
|
// email_confirm_code
|
||||||
email_confirm_code,
|
email_confirm_code,
|
||||||
//email_confirm_token
|
//email_confirm_token
|
||||||
email_confirm_token,
|
email_confirm_token,
|
||||||
// referred_by
|
// referred_by
|
||||||
...(referred_by_user ? [referred_by_user.id] : []),
|
...(referred_by_user ? [referred_by_user.id] : []),
|
||||||
// id
|
// id
|
||||||
req.user.id
|
req.user.id
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
invalidate_cached_user(req.user);
|
invalidate_cached_user(req.user);
|
||||||
|
|
||||||
// Update root directory name
|
// Update root directory name
|
||||||
await db.write(
|
await db.write(
|
||||||
`UPDATE fsentries SET name = ? WHERE user_id = ? and parent_uid IS NULL`,
|
`UPDATE fsentries SET name = ? WHERE user_id = ? and parent_uid IS NULL`,
|
||||||
[
|
[
|
||||||
// name
|
// name
|
||||||
req.body.username,
|
req.body.username,
|
||||||
// id
|
// id
|
||||||
req.user.id,
|
req.user.id,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
const filesystem = req.services.get('filesystem');
|
const filesystem = req.services.get('filesystem');
|
||||||
await filesystem.update_child_paths(`/${req.user.username}`, `/${req.body.username}`, req.user.id);
|
await filesystem.update_child_paths(`/${req.user.username}`, `/${req.body.username}`, req.user.id);
|
||||||
|
|
||||||
if(req.body.send_confirmation_code)
|
if(req.body.send_confirmation_code)
|
||||||
send_email_verification_code(email_confirm_code, req.body.email);
|
send_email_verification_code(email_confirm_code, req.body.email);
|
||||||
else
|
else
|
||||||
send_email_verification_token(email_confirm_token, req.body.email, user_uuid);
|
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,
|
|
||||||
}
|
}
|
||||||
})
|
|
||||||
|
// 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
|
module.exports = router
|
105
packages/backend/src/services/LockService.js
Normal file
105
packages/backend/src/services/LockService.js
Normal file
@ -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 };
|
@ -23,6 +23,7 @@ const BaseService = require("../BaseService");
|
|||||||
const MODE_READ = Symbol('read');
|
const MODE_READ = Symbol('read');
|
||||||
const MODE_WRITE = Symbol('write');
|
const MODE_WRITE = Symbol('write');
|
||||||
|
|
||||||
|
// TODO: DRY: could use LockService now
|
||||||
class FSLockService extends BaseService {
|
class FSLockService extends BaseService {
|
||||||
async _construct () {
|
async _construct () {
|
||||||
this.locks = {};
|
this.locks = {};
|
||||||
|
Loading…
Reference in New Issue
Block a user