Files
cloudron-box/src/users.js

1054 lines
41 KiB
JavaScript
Raw Normal View History

'use strict';
exports = module.exports = {
2021-04-14 15:54:09 -07:00
removePrivateFields,
2021-07-15 09:50:11 -07:00
add,
createOwner,
isActivated,
2021-08-20 11:30:35 -07:00
list,
listPaged,
2021-04-14 15:54:09 -07:00
get,
getByInviteToken,
2021-04-14 15:54:09 -07:00
getByResetToken,
getByUsername,
2021-07-19 12:43:30 -07:00
getByEmail,
2021-07-15 09:50:11 -07:00
getOwner,
2021-04-14 15:54:09 -07:00
getAdmins,
getSuperadmins,
2021-07-15 09:50:11 -07:00
verifyWithId,
2021-07-15 09:50:11 -07:00
verifyWithUsername,
verifyWithEmail,
2021-04-14 15:54:09 -07:00
setPassword,
2021-09-17 12:52:41 +02:00
setGhost,
updateProfile,
2021-04-14 15:54:09 -07:00
update,
2021-07-15 09:50:11 -07:00
del,
2021-04-14 15:54:09 -07:00
setTwoFactorAuthenticationSecret,
enableTwoFactorAuthentication,
disableTwoFactorAuthentication,
sendPasswordResetByIdentifier,
2021-10-27 18:36:28 +02:00
getPasswordResetLink,
sendPasswordResetEmail,
2021-10-27 19:58:06 +02:00
getInviteLink,
sendInviteEmail,
2021-07-15 09:50:11 -07:00
notifyLoginLocation,
setupAccount,
2021-07-15 09:50:11 -07:00
setAvatar,
2021-04-29 12:49:48 -07:00
getAvatar,
2022-05-14 19:41:32 +02:00
getBackgroundImage,
setBackgroundImage,
2024-12-11 18:24:20 +01:00
setNotificationConfig,
2024-12-04 09:48:25 +01:00
resetSources,
parseDisplayName,
2020-01-31 15:28:42 -08:00
AP_MAIL: 'mail',
AP_WEBADMIN: 'webadmin',
ROLE_ADMIN: 'admin',
ROLE_USER: 'user',
ROLE_USER_MANAGER: 'usermanager',
2021-12-01 09:27:24 -08:00
ROLE_MAIL_MANAGER: 'mailmanager',
ROLE_OWNER: 'owner',
2021-04-14 15:54:09 -07:00
compareRoles,
};
2021-06-25 22:11:17 -07:00
const appPasswords = require('./apppasswords.js'),
assert = require('node:assert'),
2019-10-22 16:34:17 -07:00
BoxError = require('./boxerror.js'),
crypto = require('node:crypto'),
constants = require('./constants.js'),
dashboard = require('./dashboard.js'),
2021-06-25 22:11:17 -07:00
database = require('./database.js'),
2016-05-29 23:15:55 -07:00
debug = require('debug')('box:user'),
2016-05-01 20:01:34 -07:00
eventlog = require('./eventlog.js'),
2019-10-25 15:58:11 -07:00
externalLdap = require('./externalldap.js'),
hat = require('./hat.js'),
mail = require('./mail.js'),
2016-02-08 15:15:42 -08:00
mailer = require('./mailer.js'),
mysql = require('mysql2'),
notifications = require('./notifications'),
2025-06-11 22:53:29 +02:00
oidcClients = require('./oidcclients.js'),
2018-04-25 19:08:15 +02:00
qrcode = require('qrcode'),
safe = require('safetydance'),
settings = require('./settings.js'),
2018-04-25 19:08:15 +02:00
speakeasy = require('speakeasy'),
tokens = require('./tokens.js'),
translations = require('./translations.js'),
uaParser = require('ua-parser-js'),
userDirectory = require('./user-directory.js'),
2025-07-10 10:55:52 +02:00
superagent = require('@cloudron/superagent'),
util = require('node:util'),
2025-03-07 12:07:33 +01:00
validator = require('./validator.js'),
_ = require('./underscore.js');
// the avatar and backgroundImage fields are special and not added here to reduce response sizes
const USERS_FIELDS = [ 'id', 'username', 'email', 'fallbackEmail', 'password', 'salt', 'creationTime', 'inviteToken', 'resetToken', 'displayName', 'language',
'twoFactorAuthenticationEnabled', 'twoFactorAuthenticationSecret', 'active', 'source', 'role', 'resetTokenCreationTime', 'loginLocationsJson', 'notificationConfigJson' ].join(',');
const DEFAULT_GHOST_LIFETIME = 6 * 60 * 60 * 1000; // 6 hours
2021-06-25 22:11:17 -07:00
const CRYPTO_SALT_SIZE = 64; // 512-bit salt
const CRYPTO_ITERATIONS = 10000; // iterations
const CRYPTO_KEY_LENGTH = 512; // bits
const CRYPTO_DIGEST = 'sha1'; // used to be the default in node 4.1.1 cannot change since it will affect existing db records
2021-07-15 09:50:11 -07:00
const pbkdf2Async = util.promisify(crypto.pbkdf2);
2021-08-13 14:43:08 -07:00
const randomBytesAsync = util.promisify(crypto.randomBytes);
2021-07-15 09:50:11 -07:00
function postProcess(result) {
assert.strictEqual(typeof result, 'object');
result.twoFactorAuthenticationEnabled = !!result.twoFactorAuthenticationEnabled;
result.active = !!result.active;
result.loginLocations = safe.JSON.parse(result.loginLocationsJson) || [];
if (!Array.isArray(result.loginLocations)) result.loginLocations = [];
delete result.loginLocationsJson;
2024-12-11 18:24:20 +01:00
result.notificationConfig = safe.JSON.parse(result.notificationConfigJson) || [];
delete result.notificationConfigJson;
2021-07-15 09:50:11 -07:00
return result;
}
// keep this in sync with validateGroupname and validateAlias
function validateUsername(username) {
assert.strictEqual(typeof username, 'string');
2019-10-24 14:40:26 -07:00
if (username.length < 1) return new BoxError(BoxError.BAD_FIELD, 'Username must be atleast 1 char');
if (username.length >= 200) return new BoxError(BoxError.BAD_FIELD, 'Username too long');
2019-10-24 14:40:26 -07:00
if (constants.RESERVED_NAMES.indexOf(username) !== -1) return new BoxError(BoxError.BAD_FIELD, 'Username is reserved');
2016-04-13 16:50:20 -07:00
2020-07-23 16:29:54 -07:00
// also need to consider valid LDAP characters here (e.g '+' is reserved). apps like openvpn require _ to not be used
2019-10-24 14:40:26 -07:00
if (/[^a-zA-Z0-9.-]/.test(username)) return new BoxError(BoxError.BAD_FIELD, 'Username can only contain alphanumerals, dot and -');
2016-05-25 21:36:20 -07:00
// app emails are sent using the .app suffix
if (username.endsWith('.app')) return new BoxError(BoxError.BAD_FIELD, 'Username pattern is reserved for apps');
2016-05-18 21:45:02 -07:00
return null;
}
function validateEmail(email) {
assert.strictEqual(typeof email, 'string');
2019-10-24 14:40:26 -07:00
if (!validator.isEmail(email)) return new BoxError(BoxError.BAD_FIELD, 'Invalid email');
return null;
}
function validateToken(token) {
assert.strictEqual(typeof token, 'string');
2019-10-24 14:40:26 -07:00
if (token.length !== 64) return new BoxError(BoxError.BAD_FIELD, 'Invalid token'); // 256-bit hex coded token
return null;
}
function validateDisplayName(name) {
assert.strictEqual(typeof name, 'string');
return null;
}
async function validateLanguage(language) {
assert.strictEqual(typeof language, 'string');
if (language === '') return null; // reset to platform default
const languages = await translations.listLanguages();
if (!languages.includes(language)) return new BoxError(BoxError.BAD_FIELD, 'Invalid language');
return null;
}
function validatePassword(password) {
assert.strictEqual(typeof password, 'string');
2019-10-24 14:40:26 -07:00
if (password.length < 8) return new BoxError(BoxError.BAD_FIELD, 'Password must be atleast 8 characters');
if (password.length > 256) return new BoxError(BoxError.BAD_FIELD, 'Password cannot be more than 256 characters');
return null;
}
// remove all fields that should never be sent out via REST API
function removePrivateFields(user) {
const result = _.pick(user, [
'id', 'username', 'email', 'fallbackEmail', 'displayName', 'groupIds', 'active', 'source', 'role', 'createdAt',
'twoFactorAuthenticationEnabled', 'notificationConfig']);
2021-10-01 14:32:37 +02:00
// invite status indicator
result.inviteAccepted = !user.inviteToken;
return result;
}
function validateRole(role) {
assert.strictEqual(typeof role, 'string');
const ORDERED_ROLES = [ exports.ROLE_USER, exports.ROLE_USER_MANAGER, exports.ROLE_MAIL_MANAGER, exports.ROLE_ADMIN, exports.ROLE_OWNER ];
if (ORDERED_ROLES.includes(role)) return null;
return new BoxError(BoxError.BAD_FIELD, `Invalid role '${role}'`);
}
function compareRoles(role1, role2) {
assert.strictEqual(typeof role1, 'string');
assert.strictEqual(typeof role2, 'string');
const ORDERED_ROLES = [ exports.ROLE_USER, exports.ROLE_USER_MANAGER, exports.ROLE_MAIL_MANAGER, exports.ROLE_ADMIN, exports.ROLE_OWNER ];
const roleInt1 = ORDERED_ROLES.indexOf(role1);
const roleInt2 = ORDERED_ROLES.indexOf(role2);
return roleInt1 - roleInt2;
}
2021-07-15 09:50:11 -07:00
async function add(email, data, auditSource) {
assert.strictEqual(typeof email, 'string');
2021-07-15 09:50:11 -07:00
assert(data && typeof data === 'object');
assert(auditSource && typeof auditSource === 'object');
2016-02-08 21:05:02 -08:00
2021-07-15 09:50:11 -07:00
assert(data.username === null || typeof data.username === 'string');
assert(data.password === null || typeof data.password === 'string');
assert.strictEqual(typeof data.displayName, 'string');
if ('fallbackEmail' in data) assert.strictEqual(typeof data.fallbackEmail, 'string');
2021-07-15 09:50:11 -07:00
2024-05-25 13:42:29 +02:00
const { displayName } = data;
let { username, password } = data;
let fallbackEmail = data.fallbackEmail || '';
2021-07-15 09:50:11 -07:00
const source = data.source || ''; // empty is local user
const role = data.role || exports.ROLE_USER;
const notificationConfig = 'notificationConfig' in data ? data.notificationConfig : null;
let error;
if (username !== null) {
username = username.toLowerCase();
error = validateUsername(username);
2021-07-15 09:50:11 -07:00
if (error) throw error;
}
if (password !== null) {
error = validatePassword(password);
2021-07-15 09:50:11 -07:00
if (error) throw error;
} else {
password = hat(8 * 8);
}
email = email.toLowerCase();
error = validateEmail(email);
2021-07-15 09:50:11 -07:00
if (error) throw error;
fallbackEmail = fallbackEmail.toLowerCase();
if (fallbackEmail) {
2024-04-26 20:09:36 +02:00
error = validateEmail(fallbackEmail);
if (error) throw error;
}
error = validateDisplayName(displayName);
2021-07-15 09:50:11 -07:00
if (error) throw error;
error = validateRole(role);
2021-07-15 09:50:11 -07:00
if (error) throw error;
2024-05-25 13:42:29 +02:00
const [randomBytesError, salt] = await safe(randomBytesAsync(CRYPTO_SALT_SIZE));
if (randomBytesError) throw new BoxError(BoxError.CRYPTO_ERROR, randomBytesError);
2021-07-15 09:50:11 -07:00
2024-05-25 13:42:29 +02:00
const [pbkdf2Error, derivedKey] = await safe(pbkdf2Async(password, salt, CRYPTO_ITERATIONS, CRYPTO_KEY_LENGTH, CRYPTO_DIGEST));
if (pbkdf2Error) throw new BoxError(BoxError.CRYPTO_ERROR, pbkdf2Error);
2021-07-15 09:50:11 -07:00
const user = {
id: 'uid-' + crypto.randomUUID(),
2024-05-25 13:42:29 +02:00
username,
email,
fallbackEmail,
2021-07-15 09:50:11 -07:00
password: Buffer.from(derivedKey, 'binary').toString('hex'),
salt: salt.toString('hex'),
resetToken: '',
2021-10-01 14:45:26 +02:00
inviteToken: hat(256), // new users start out with invite tokens
2024-05-25 13:42:29 +02:00
displayName,
source,
role,
avatar: null,
language: '',
notificationConfigJson: notificationConfig ? JSON.stringify(notificationConfig) : null
2021-07-15 09:50:11 -07:00
};
const query = 'INSERT INTO users (id, username, password, email, fallbackEmail, salt, resetToken, inviteToken, displayName, source, role, avatar, language, notificationConfigJson) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)';
const args = [ user.id, user.username, user.password, user.email, user.fallbackEmail, user.salt, user.resetToken, user.inviteToken, user.displayName, user.source, user.role, user.avatar, user.language, user.notificationConfigJson ];
2021-07-15 09:50:11 -07:00
[error] = await safe(database.query(query, args));
2025-09-29 11:55:15 +02:00
if (error && error.sqlCode === 'ER_DUP_ENTRY' && error.sqlMessage.indexOf('users_email') !== -1) throw new BoxError(BoxError.ALREADY_EXISTS, 'email already exists');
if (error && error.sqlCode === 'ER_DUP_ENTRY' && error.sqlMessage.indexOf('users_username') !== -1) throw new BoxError(BoxError.ALREADY_EXISTS, 'username already exists');
if (error && error.sqlCode === 'ER_DUP_ENTRY' && error.sqlMessage.indexOf('PRIMARY') !== -1) throw new BoxError(BoxError.ALREADY_EXISTS, 'id already exists');
2021-07-15 09:50:11 -07:00
if (error) throw error;
// when this is used to create the owner, then we have to patch the auditSource to contain himself
if (!auditSource.userId) auditSource.userId = user.id;
if (!auditSource.username) auditSource.username= user.username;
2022-02-24 20:04:46 -08:00
await eventlog.add(eventlog.ACTION_USER_ADD, auditSource, { userId: user.id, email: user.email, user: removePrivateFields(user) });
2021-07-15 09:50:11 -07:00
2021-07-19 12:43:30 -07:00
return user.id;
}
2021-06-26 09:57:07 -07:00
async function del(user, auditSource) {
assert.strictEqual(typeof user, 'object');
assert(auditSource && typeof auditSource === 'object');
if (constants.DEMO && user.username === constants.DEMO_USERNAME) throw new BoxError(BoxError.BAD_STATE, 'Not allowed in demo mode');
const arSearch = `JSON_SEARCH(accessRestrictionJson, 'one', ?, NULL, '$.users')`;
const opSearch = `JSON_SEARCH(operatorsJson, 'one', ?, NULL, '$.users')`;
const queries = [
{ query: `UPDATE apps SET accessRestrictionJson=JSON_REMOVE(accessRestrictionJson, REPLACE(${arSearch}, '"', '')) WHERE ${arSearch} IS NOT NULL`, args: [ user.id, user.id ] },
{ query: `UPDATE apps SET operatorsJson=JSON_REMOVE(operatorsJson, REPLACE(${opSearch}, '"', '')) WHERE ${opSearch} IS NOT NULL`, args: [ user.id, user.id ] },
{ query: 'DELETE FROM groupMembers WHERE userId = ?', args: [ user.id ] },
{ query: 'DELETE FROM tokens WHERE identifier = ?', args: [ user.id ] },
{ query: 'DELETE FROM appPasswords WHERE userId = ?', args: [ user.id ] },
{ query: 'DELETE FROM users WHERE id = ?', args: [ user.id ] }, // keep this the last query as we check affectedRows below
];
2021-06-26 09:57:07 -07:00
const [error, result] = await safe(database.transaction(queries));
2025-09-29 11:55:15 +02:00
if (error && error.sqlCode === 'ER_NO_REFERENCED_ROW_2') throw new BoxError(BoxError.NOT_FOUND, error);
2021-06-26 09:57:07 -07:00
if (error) throw error;
if (result[queries.length-1].affectedRows !== 1) throw new BoxError(BoxError.NOT_FOUND, 'User not found');
2021-06-03 11:42:32 -07:00
2022-02-24 20:04:46 -08:00
await eventlog.add(eventlog.ACTION_USER_REMOVE, auditSource, { userId: user.id, user: removePrivateFields(user) });
}
2021-08-20 11:30:35 -07:00
async function list() {
2021-07-15 09:50:11 -07:00
const results = await database.query(`SELECT ${USERS_FIELDS},GROUP_CONCAT(groupMembers.groupId) AS groupIds ` +
' FROM users LEFT OUTER JOIN groupMembers ON users.id = groupMembers.userId ' +
' GROUP BY users.id ORDER BY users.username');
2016-02-09 09:25:17 -08:00
2021-07-15 09:50:11 -07:00
results.forEach(function (result) {
result.groupIds = result.groupIds ? result.groupIds.split(',') : [ ];
2016-02-09 09:25:17 -08:00
});
2021-07-15 09:50:11 -07:00
results.forEach(postProcess);
return results;
2016-02-09 09:25:17 -08:00
}
2022-02-07 16:57:00 +01:00
// if active is null then both active and inactive users are listed
async function listPaged(search, active, page, perPage) {
assert(typeof search === 'string' || search === null);
2022-02-07 16:57:00 +01:00
assert(typeof active === 'boolean' || active === null);
2019-01-14 16:39:20 +01:00
assert.strictEqual(typeof page, 'number');
assert.strictEqual(typeof perPage, 'number');
2021-07-15 09:50:11 -07:00
let query = `SELECT ${USERS_FIELDS},GROUP_CONCAT(groupMembers.groupId) AS groupIds FROM users LEFT OUTER JOIN groupMembers ON users.id = groupMembers.userId `;
2019-01-14 16:39:20 +01:00
2021-07-15 09:50:11 -07:00
if (search) {
query += ' WHERE ';
2022-02-07 16:57:00 +01:00
query += '(';
2021-07-15 09:50:11 -07:00
query += '(LOWER(users.username) LIKE ' + mysql.escape(`%${search.toLowerCase()}%`) + ')';
query += ' OR ';
query += '(LOWER(users.email) LIKE ' + mysql.escape(`%${search.toLowerCase()}%`) + ')';
query += ' OR ';
query += '(LOWER(users.displayName) LIKE ' + mysql.escape(`%${search.toLowerCase()}%`) + ')';
2022-02-07 16:57:00 +01:00
query += ')';
}
if (active !== null) {
if (search) query += ' AND ';
else query += ' WHERE ';
query += 'users.active' + (!active ? ' IS NOT ' : ' IS ') + 'TRUE';
2021-07-15 09:50:11 -07:00
}
2019-01-14 16:39:20 +01:00
2021-07-15 09:50:11 -07:00
query += ` GROUP BY users.id ORDER BY users.username ASC LIMIT ${(page-1)*perPage},${perPage} `;
2016-06-07 09:59:29 -07:00
2021-07-15 09:50:11 -07:00
const results = await database.query(query);
2016-06-07 09:59:29 -07:00
2021-07-15 09:50:11 -07:00
results.forEach(function (result) {
result.groupIds = result.groupIds ? result.groupIds.split(',') : [ ];
2016-06-07 09:59:29 -07:00
});
2021-07-15 09:50:11 -07:00
results.forEach(postProcess);
2021-07-15 09:50:11 -07:00
return results;
}
2021-07-15 09:50:11 -07:00
async function isActivated() {
const result = await database.query('SELECT COUNT(*) AS total FROM users');
return result[0].total !== 0;
}
async function internalGet(fieldName, fieldValue) {
assert.strictEqual(typeof fieldName, 'string');
assert.strictEqual(typeof fieldValue, 'string');
2021-07-15 09:50:11 -07:00
const results = await database.query(`SELECT ${USERS_FIELDS},GROUP_CONCAT(groupMembers.groupId) AS groupIds ` +
' FROM users LEFT OUTER JOIN groupMembers ON users.id = groupMembers.userId ' +
` GROUP BY users.id HAVING users.${fieldName} = ?`, [ fieldValue ]);
2021-07-15 09:50:11 -07:00
if (results.length === 0) return null;
2016-02-08 20:38:50 -08:00
2021-07-15 09:50:11 -07:00
results[0].groupIds = results[0].groupIds ? results[0].groupIds.split(',') : [ ];
2016-02-08 20:38:50 -08:00
2021-07-15 09:50:11 -07:00
return postProcess(results[0]);
}
async function get(userId) {
assert.strictEqual(typeof userId, 'string');
return await internalGet('id', userId);
}
async function getByEmail(email) {
assert.strictEqual(typeof email, 'string');
return await internalGet('email', email);
2021-07-15 09:50:11 -07:00
}
async function getByResetToken(resetToken) {
assert.strictEqual(typeof resetToken, 'string');
2024-04-26 20:09:36 +02:00
const error = validateToken(resetToken);
2021-07-15 09:50:11 -07:00
if (error) throw error;
return await internalGet('resetToken', resetToken);
}
async function getByInviteToken(inviteToken) {
assert.strictEqual(typeof inviteToken, 'string');
2024-04-26 20:09:36 +02:00
const error = validateToken(inviteToken);
if (error) throw error;
return await internalGet('inviteToken', inviteToken);
}
2021-07-15 09:50:11 -07:00
async function getByUsername(username) {
2019-03-18 21:15:50 -07:00
assert.strictEqual(typeof username, 'string');
return await internalGet('username', username);
2019-03-18 21:15:50 -07:00
}
2021-07-15 09:50:11 -07:00
async function update(user, data, auditSource) {
assert.strictEqual(typeof user, 'object');
assert.strictEqual(typeof data, 'object');
assert(auditSource && typeof auditSource === 'object');
2021-07-15 09:50:11 -07:00
assert(!('twoFactorAuthenticationEnabled' in data) || (typeof data.twoFactorAuthenticationEnabled === 'boolean'));
assert(!('active' in data) || (typeof data.active === 'boolean'));
assert(!('loginLocations' in data) || (Array.isArray(data.loginLocations)));
if (constants.DEMO && user.username === constants.DEMO_USERNAME) throw new BoxError(BoxError.BAD_STATE, 'Not allowed in demo mode');
2021-07-15 09:50:11 -07:00
if (data.username) {
2022-01-13 15:20:16 -08:00
// regardless of "account setup", username cannot be changed because admin could have logged in with temp password and apps
// already know about it
if (user.username) throw new BoxError(BoxError.CONFLICT, 'Username cannot be changed');
data.username = data.username.toLowerCase();
2024-05-25 13:42:29 +02:00
const error = validateUsername(data.username);
2021-07-15 09:50:11 -07:00
if (error) throw error;
}
if (data.email) {
data.email = data.email.toLowerCase();
2024-05-25 13:42:29 +02:00
const error = validateEmail(data.email);
2021-07-15 09:50:11 -07:00
if (error) throw error;
}
2018-01-21 14:25:39 +01:00
if (data.fallbackEmail) {
data.fallbackEmail = data.fallbackEmail.toLowerCase();
2024-05-25 13:42:29 +02:00
const error = validateEmail(data.fallbackEmail);
2021-07-15 09:50:11 -07:00
if (error) throw error;
2018-01-21 14:25:39 +01:00
}
if (data.role) {
2024-05-25 13:42:29 +02:00
const error = validateRole(data.role);
2021-07-15 09:50:11 -07:00
if (error) throw error;
2020-02-13 22:06:54 -08:00
}
if (data.language) {
2024-05-25 13:42:29 +02:00
const error = await validateLanguage(data.language);
if (error) throw error;
}
2024-05-25 13:42:29 +02:00
const args = [], fields = [];
2021-07-15 09:50:11 -07:00
for (const k in data) {
if (k === 'twoFactorAuthenticationEnabled' || k === 'active') {
fields.push(k + ' = ?');
args.push(data[k] ? 1 : 0);
} else if (k === 'loginLocations' || k === 'notificationConfig') {
fields.push(`${k}Json = ?`);
2021-07-15 09:50:11 -07:00
args.push(JSON.stringify(data[k]));
} else {
fields.push(k + ' = ?');
args.push(data[k]);
}
}
if (args.length == 0) return; // nothing to do
2021-07-15 09:50:11 -07:00
args.push(user.id);
2024-05-25 13:42:29 +02:00
const [error, result] = await safe(database.query('UPDATE users SET ' + fields.join(', ') + ' WHERE id = ?', args));
2025-09-29 11:55:15 +02:00
if (error && error.sqlCode === 'ER_DUP_ENTRY' && error.sqlMessage.indexOf('users_email') !== -1) throw new BoxError(BoxError.ALREADY_EXISTS, 'email already exists');
if (error && error.sqlCode === 'ER_DUP_ENTRY' && error.sqlMessage.indexOf('users_username') !== -1) throw new BoxError(BoxError.ALREADY_EXISTS, 'username already exists');
2021-07-15 09:50:11 -07:00
if (error) throw new BoxError(BoxError.DATABASE_ERROR, error);
if (result.affectedRows !== 1) throw new BoxError(BoxError.NOT_FOUND, 'User not found');
const newUser = Object.assign({}, user, data);
2020-02-14 13:01:51 -08:00
2022-02-24 20:04:46 -08:00
await eventlog.add(eventlog.ACTION_USER_UPDATE, auditSource, {
2021-07-15 09:50:11 -07:00
userId: user.id,
user: removePrivateFields(newUser),
roleChanged: newUser.role !== user.role,
activeStatusChanged: ((newUser.active && !user.active) || (!newUser.active && user.active))
});
}
async function updateProfile(user, profile, auditSource) {
assert.strictEqual(typeof user, 'object');
assert.strictEqual(typeof profile, 'object');
assert(auditSource && typeof auditSource === 'object');
if (user.source === 'ldap') throw new BoxError(BoxError.BAD_STATE, 'Cannot update profile of external auth user');
await update(user, profile, auditSource);
}
async function listByRole(role) {
assert.strictEqual(typeof role, 'string');
// the mailer code relies on the first object being the 'owner' (thus the ORDER)
const results = await database.query(`SELECT ${USERS_FIELDS},GROUP_CONCAT(groupMembers.groupId) AS groupIds ` +
' FROM users LEFT OUTER JOIN groupMembers ON users.id = groupMembers.userId ' +
' GROUP BY users.id HAVING role = ? ORDER BY users.creationTime', [ role ]);
results.forEach(function (result) {
result.groupIds = result.groupIds ? result.groupIds.split(',') : [ ];
});
results.forEach(postProcess);
return results;
}
2021-07-15 09:50:11 -07:00
async function getOwner() {
const owners = await listByRole(exports.ROLE_OWNER);
2021-07-15 09:50:11 -07:00
if (owners.length === 0) return null;
return owners[0];
2016-02-09 15:47:02 -08:00
}
2021-07-15 09:50:11 -07:00
async function getAdmins() {
const owners = await listByRole(exports.ROLE_OWNER);
const admins = await listByRole(exports.ROLE_ADMIN);
2021-07-15 09:50:11 -07:00
return owners.concat(admins);
2016-01-15 16:04:33 +01:00
}
2021-07-15 09:50:11 -07:00
async function getSuperadmins() {
return await listByRole(exports.ROLE_OWNER);
}
async function setGhost(user, password, expiresAt) {
assert.strictEqual(typeof user, 'object');
assert.strictEqual(typeof password, 'string');
assert.strictEqual(typeof expiresAt, 'number');
if (!user.username) throw new BoxError(BoxError.BAD_STATE, 'user has no username yet');
expiresAt = expiresAt || (Date.now() + DEFAULT_GHOST_LIFETIME);
const ghostData = await settings.getJson(settings.GHOSTS_CONFIG_KEY) || {};
ghostData[user.username] = { password, expiresAt };
await settings.setJson(settings.GHOSTS_CONFIG_KEY, ghostData);
}
// returns true if ghost user was matched
async function verifyGhost(username, password) {
assert.strictEqual(typeof username, 'string');
assert.strictEqual(typeof password, 'string');
const ghostData = await settings.getJson(settings.GHOSTS_CONFIG_KEY) || {};
// either the username is an object with { password, expiresAt } or a string with the password which will expire on first match
if (username in ghostData) {
if (typeof ghostData[username] === 'object') {
if (ghostData[username].expiresAt < Date.now()) {
debug('verifyGhost: password expired');
delete ghostData[username];
await settings.setJson(settings.GHOSTS_CONFIG_KEY, ghostData);
return false;
} else if (ghostData[username].password === password) {
debug('verifyGhost: matched ghost user');
return true;
} else {
return false;
}
} else if(ghostData[username] === password) {
debug('verifyGhost: matched ghost user');
delete ghostData[username];
await settings.setJson(settings.GHOSTS_CONFIG_KEY, ghostData);
return true;
}
}
return false;
}
async function verifyAppPassword(userId, password, identifier) {
assert.strictEqual(typeof userId, 'string');
assert.strictEqual(typeof password, 'string');
assert.strictEqual(typeof identifier, 'string');
const results = await appPasswords.list(userId);
const hashedPasswords = results.filter(r => r.identifier === identifier).map(r => r.hashedPassword);
const hash = crypto.createHash('sha256').update(password).digest('base64');
if (hashedPasswords.includes(hash)) return;
throw new BoxError(BoxError.INVALID_CREDENTIALS, 'Password is not valid');
}
// identifier is only used to check if password is valid for a specific app
async function verify(user, password, identifier, options) {
assert.strictEqual(typeof user, 'object');
assert.strictEqual(typeof password, 'string');
assert.strictEqual(typeof identifier, 'string');
assert.strictEqual(typeof options, 'object');
if (!user.active) {
debug(`verify: ${user.username} is not active`);
throw new BoxError(BoxError.NOT_FOUND, 'User not active');
}
// for just invited users the username may be still null
if (user.username) {
const valid = await verifyGhost(user.username, password);
if (valid) {
debug(`verify: ${user.username} authenticated via impersonation`);
user.ghost = true;
return user;
}
}
const [error] = await safe(verifyAppPassword(user.id, password, identifier));
if (!error) { // matched app password
debug(`verify: ${user.username || user.id} matched app password`);
user.appPassword = true;
return user;
}
let localTotpCheck = true; // does 2fa need to be verified with local database 2fa creds
if (user.source === 'ldap') {
await externalLdap.verifyPassword(user.username, password, options);
const externalLdapConfig = await externalLdap.getConfig();
localTotpCheck = user.twoFactorAuthenticationEnabled && !externalLdap.supports2FA(externalLdapConfig);
} else {
const saltBinary = Buffer.from(user.salt, 'hex');
const [error, derivedKey] = await safe(pbkdf2Async(password, saltBinary, CRYPTO_ITERATIONS, CRYPTO_KEY_LENGTH, CRYPTO_DIGEST));
if (error) throw new BoxError(BoxError.CRYPTO_ERROR, error);
const derivedKeyHex = Buffer.from(derivedKey, 'binary').toString('hex');
if (derivedKeyHex !== user.password) {
debug(`verify: ${user.username || user.id} provided incorrect password`);
throw new BoxError(BoxError.INVALID_CREDENTIALS, 'Wrong password');
}
localTotpCheck = user.twoFactorAuthenticationEnabled;
}
if (localTotpCheck && !options.skipTotpCheck) {
if (!options.totpToken) throw new BoxError(BoxError.INVALID_CREDENTIALS, 'A totpToken must be provided');
const verified = speakeasy.totp.verify({ secret: user.twoFactorAuthenticationSecret, encoding: 'base32', token: options.totpToken, window: 2 });
if (!verified) throw new BoxError(BoxError.INVALID_CREDENTIALS, 'Invalid totpToken');
}
return user;
}
async function verifyWithId(id, password, identifier, options) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof password, 'string');
assert.strictEqual(typeof identifier, 'string');
assert.strictEqual(typeof options, 'object');
const user = await get(id);
if (!user) {
debug(`verifyWithId: ${id} not found`);
throw new BoxError(BoxError.NOT_FOUND, 'User not found');
}
return await verify(user, password, identifier, options);
}
async function verifyWithUsername(username, password, identifier, options) {
assert.strictEqual(typeof username, 'string');
assert.strictEqual(typeof password, 'string');
assert.strictEqual(typeof identifier, 'string');
assert.strictEqual(typeof options, 'object');
const user = await getByUsername(username.toLowerCase());
if (user) return await verify(user, password, identifier, options);
const [error, newUserId] = await safe(externalLdap.maybeCreateUser(username.toLowerCase()));
if (error && error.reason === BoxError.BAD_STATE) {
debug(`verifyWithUsername: ${username} not found`);
throw new BoxError(BoxError.NOT_FOUND, 'User not found'); // no external ldap or no auto create
}
if (error) {
debug(`verifyWithUsername: failed to auto create user ${username}. %o`, error);
throw new BoxError(BoxError.NOT_FOUND, 'User not found');
}
return await verifyWithId(newUserId, password, identifier, options);
}
async function verifyWithEmail(email, password, identifier, options) {
assert.strictEqual(typeof email, 'string');
assert.strictEqual(typeof password, 'string');
assert.strictEqual(typeof identifier, 'string');
assert.strictEqual(typeof options, 'object');
const user = await getByEmail(email.toLowerCase());
if (!user) {
debug(`verifyWithEmail: ${email} no such user`);
throw new BoxError(BoxError.NOT_FOUND, 'User not found');
}
return await verify(user, password, identifier, options);
}
2021-10-27 18:36:28 +02:00
async function getPasswordResetLink(user, auditSource) {
assert.strictEqual(typeof user, 'object');
assert.strictEqual(typeof auditSource, 'object');
let resetToken = user.resetToken;
let resetTokenCreationTime = user.resetTokenCreationTime || 0;
if (!resetToken || (Date.now() - resetTokenCreationTime > 7 * 24 * 60 * 60 * 1000)) {
resetToken = hat(256);
resetTokenCreationTime = new Date();
await update(user, { resetToken, resetTokenCreationTime }, auditSource);
}
const { fqdn:dashboardFqdn } = await dashboard.getLocation();
const resetLink = `https://${dashboardFqdn}/passwordreset.html?resetToken=${resetToken}`;
return resetLink;
}
async function sendPasswordResetByIdentifier(identifier, auditSource) {
assert.strictEqual(typeof identifier, 'string');
assert.strictEqual(typeof auditSource, 'object');
const user = identifier.indexOf('@') === -1 ? await getByUsername(identifier.toLowerCase()) : await getByEmail(identifier.toLowerCase());
if (!user) throw new BoxError(BoxError.NOT_FOUND, 'User not found');
if (user.source === 'ldap') throw new BoxError(BoxError.BAD_STATE, 'Cannot reset password of external auth user');
const email = user.fallbackEmail || user.email;
// security measure to prevent a mail manager or admin resetting the superadmin's password
const mailDomains = await mail.listDomains();
2022-11-10 13:46:33 +01:00
if (mailDomains.some(d => d.enabled && email.endsWith(`@${d.domain}`))) throw new BoxError(BoxError.CONFLICT, 'Password reset email cannot be sent to email addresses hosted on the same Cloudron');
const resetLink = await getPasswordResetLink(user, auditSource);
await mailer.passwordReset(user, email, resetLink);
}
2021-10-27 18:36:28 +02:00
async function sendPasswordResetEmail(user, email, auditSource) {
assert.strictEqual(typeof user, 'object');
assert.strictEqual(typeof email, 'string');
assert.strictEqual(typeof auditSource, 'object');
const error = validateEmail(email);
if (error) throw error;
// security measure to prevent a mail manager or admin resetting the superadmin's password
const mailDomains = await mail.listDomains();
2022-11-10 13:46:33 +01:00
if (mailDomains.some(d => d.enabled && email.endsWith(`@${d.domain}`))) throw new BoxError(BoxError.CONFLICT, 'Password reset email cannot be sent to email addresses hosted on the same Cloudron');
2021-10-27 18:36:28 +02:00
const resetLink = await getPasswordResetLink(user, auditSource);
await mailer.passwordReset(user, email, resetLink);
}
2021-07-15 09:50:11 -07:00
async function notifyLoginLocation(user, ip, userAgent, auditSource) {
assert.strictEqual(typeof user, 'object');
assert.strictEqual(typeof ip, 'string');
assert.strictEqual(typeof userAgent, 'string');
2021-07-15 09:50:11 -07:00
assert.strictEqual(typeof auditSource, 'object');
debug(`notifyLoginLocation: ${user.id} ${ip} ${userAgent} ghost:${!!user.ghost}`);
2023-08-04 14:13:30 +05:30
if (constants.DEMO) return;
2021-07-15 09:50:11 -07:00
if (constants.TEST && ip === '127.0.0.1') return;
if (user.ghost || user.source) return; // for external users, rely on the external source to send login notification to avoid dup login emails
const response = await superagent.get('https://geolocation.cloudron.io/json').query({ ip }).ok(() => true);
if (response.status !== 200) return debug(`Failed to get geoip info. status: ${response.status}`);
2021-04-30 09:44:25 -07:00
2021-07-15 09:50:11 -07:00
const country = safe.query(response.body, 'country.names.en', '');
const city = safe.query(response.body, 'city.names.en', '');
2021-07-15 09:50:11 -07:00
if (!city || !country) return;
2021-07-15 09:50:11 -07:00
const ua = uaParser(userAgent);
const simplifiedUserAgent = ua.browser.name ? `${ua.browser.name} - ${ua.os.name}` : userAgent;
2021-07-15 09:50:11 -07:00
const knownLogin = user.loginLocations.find(function (l) {
return l.userAgent === simplifiedUserAgent && l.country === country && l.city === city;
});
2021-07-15 09:50:11 -07:00
if (knownLogin) return;
2021-07-15 09:50:11 -07:00
// purge potentially old locations where ts > now() - 6 months
const sixMonthsBack = Date.now() - 6 * 30 * 24 * 60 * 60 * 1000;
const newLoginLocation = { ts: Date.now(), ip, userAgent: simplifiedUserAgent, country, city };
2024-04-26 20:09:36 +02:00
const loginLocations = user.loginLocations.filter(function (l) { return l.ts > sixMonthsBack; });
2021-07-15 09:50:11 -07:00
// only stash if we have a real useragent, otherwise warn the user every time
if (simplifiedUserAgent) loginLocations.push(newLoginLocation);
2021-07-15 09:50:11 -07:00
await update(user, { loginLocations }, auditSource);
await safe(mailer.sendNewLoginLocation(user, newLoginLocation), { debug });
}
2021-07-15 09:50:11 -07:00
async function setPassword(user, newPassword, auditSource) {
assert.strictEqual(typeof user, 'object');
assert.strictEqual(typeof newPassword, 'string');
2021-07-15 09:50:11 -07:00
assert.strictEqual(typeof auditSource, 'object');
2024-05-25 13:42:29 +02:00
const error = validatePassword(newPassword);
2021-07-15 09:50:11 -07:00
if (error) throw error;
if (constants.DEMO && user.username === constants.DEMO_USERNAME) throw new BoxError(BoxError.BAD_STATE, 'Not allowed in demo mode');
2021-07-15 09:50:11 -07:00
if (user.source) throw new BoxError(BoxError.CONFLICT, 'User is from an external directory');
2024-05-25 13:42:29 +02:00
const [randomBytesError, salt] = await safe(randomBytesAsync(CRYPTO_SALT_SIZE));
if (randomBytesError) throw new BoxError(BoxError.CRYPTO_ERROR, randomBytesError);
2024-05-25 13:42:29 +02:00
const [pbkdf2Error, derivedKey] = await safe(pbkdf2Async(newPassword, salt, CRYPTO_ITERATIONS, CRYPTO_KEY_LENGTH, CRYPTO_DIGEST));
if (pbkdf2Error) throw new BoxError(BoxError.CRYPTO_ERROR, pbkdf2Error);
2021-07-15 09:50:11 -07:00
const data = {
2021-08-13 14:43:08 -07:00
salt: salt.toString('hex'),
2021-07-15 09:50:11 -07:00
password: Buffer.from(derivedKey, 'binary').toString('hex'),
resetToken: ''
};
2021-07-15 09:50:11 -07:00
await update(user, data, auditSource);
}
2021-07-15 09:50:11 -07:00
async function createOwner(email, username, password, displayName, auditSource) {
assert.strictEqual(typeof email, 'string');
2016-04-04 15:14:00 +02:00
assert.strictEqual(typeof username, 'string');
assert.strictEqual(typeof password, 'string');
assert.strictEqual(typeof displayName, 'string');
assert(auditSource && typeof auditSource === 'object');
2016-04-04 15:14:00 +02:00
2021-07-15 09:50:11 -07:00
// This is only not allowed for the owner. reset of username validation happens in add()
if (username === '') throw new BoxError(BoxError.BAD_FIELD, 'Username cannot be empty');
2021-07-15 09:50:11 -07:00
const activated = await isActivated();
if (activated) throw new BoxError(BoxError.ALREADY_EXISTS, 'Cloudron already activated');
const notificationConfig = [notifications.TYPE_BACKUP_FAILED, notifications.TYPE_CERTIFICATE_RENEWAL_FAILED, notifications.TYPE_MANUAL_APP_UPDATE_NEEDED, notifications.TYPE_APP_DOWN, notifications.TYPE_CLOUDRON_UPDATE_FAILED ];
return await add(email, { username, password, fallbackEmail: '', displayName, role: exports.ROLE_OWNER, notificationConfig }, auditSource);
2016-01-13 12:28:38 -08:00
}
2016-01-18 15:16:18 +01:00
2021-10-27 19:58:06 +02:00
async function getInviteLink(user, auditSource) {
assert.strictEqual(typeof user, 'object');
2021-10-27 19:58:06 +02:00
assert.strictEqual(typeof auditSource, 'object');
2016-01-18 15:16:18 +01:00
2021-07-15 09:50:11 -07:00
if (user.source) throw new BoxError(BoxError.CONFLICT, 'User is from an external directory');
if (!user.inviteToken) throw new BoxError(BoxError.BAD_STATE, 'User already used invite link');
const directoryConfig = await userDirectory.getProfileConfig();
const { fqdn:dashboardFqdn } = await dashboard.getLocation();
let inviteLink = `https://${dashboardFqdn}/setupaccount.html?inviteToken=${user.inviteToken}&email=${encodeURIComponent(user.email)}`;
if (user.username) inviteLink += `&username=${encodeURIComponent(user.username)}`;
if (user.displayName) inviteLink += `&displayName=${encodeURIComponent(user.displayName)}`;
if (directoryConfig.lockUserProfiles) inviteLink += '&profileLocked=true';
return inviteLink;
}
2021-10-27 19:58:06 +02:00
async function sendInviteEmail(user, email, auditSource) {
assert.strictEqual(typeof user, 'object');
assert.strictEqual(typeof email, 'string');
assert.strictEqual(typeof auditSource, 'object');
const error = validateEmail(email);
if (error) throw error;
2021-10-27 21:25:43 +02:00
const inviteLink = await getInviteLink(user, auditSource);
await mailer.sendInvite(user, null /* invitor */, email, inviteLink);
2021-10-27 19:58:06 +02:00
}
2021-07-15 09:50:11 -07:00
async function setupAccount(user, data, auditSource) {
assert.strictEqual(typeof user, 'object');
assert.strictEqual(typeof data, 'object');
assert(auditSource && typeof auditSource === 'object');
2025-08-14 16:02:54 +05:30
const profileConfig = await userDirectory.getProfileConfig();
// error out if admin has not provided a username
if (profileConfig.lockUserProfiles) {
if (!user.username) throw new BoxError(BoxError.CONFLICT, 'Account cannot be setup without a username');
if (data.username) throw new BoxError(BoxError.CONFLICT, 'Username cannot be changed because profiles are locked');
}
2022-02-07 16:09:43 -08:00
const tmp = { inviteToken: '' };
if (data.username) {
const error = validateUsername(data.username);
if (error) throw error;
tmp.username = data.username;
}
if (data.displayName) {
const error = validateDisplayName(data.displayName);
if (error) throw error;
tmp.displayName = data.displayName;
}
const error = validatePassword(data.password);
if (error) throw error;
await update(user, tmp, auditSource);
await setPassword(user, data.password, auditSource);
2025-06-11 22:53:29 +02:00
const token = { clientId: oidcClients.ID_WEBADMIN, identifier: user.id, expires: Date.now() + constants.DEFAULT_TOKEN_EXPIRATION_MSECS, allowedIpRanges: '' };
2021-07-15 09:50:11 -07:00
const result = await tokens.add(token);
return result.accessToken;
2018-08-17 09:49:58 -07:00
}
async function setTwoFactorAuthenticationSecret(user, auditSource) {
assert.strictEqual(typeof user, 'object');
2021-07-15 09:50:11 -07:00
assert(auditSource && typeof auditSource === 'object');
2018-04-25 19:08:15 +02:00
const externalLdapConfig = await externalLdap.getConfig();
if (user.source === 'ldap' && externalLdap.supports2FA(externalLdapConfig)) throw new BoxError(BoxError.BAD_STATE, 'Cannot disable 2FA of external auth user');
2018-04-25 19:08:15 +02:00
if (constants.DEMO && user.username === constants.DEMO_USERNAME) throw new BoxError(BoxError.BAD_STATE, 'Not allowed in demo mode');
if (user.twoFactorAuthenticationEnabled) throw new BoxError(BoxError.ALREADY_EXISTS, '2FA is already enabled');
2018-04-25 19:08:15 +02:00
const { fqdn:dashboardFqdn } = await dashboard.getLocation();
const secret = speakeasy.generateSecret({ name: `Cloudron ${dashboardFqdn} (${user.username})` });
2018-04-25 19:08:15 +02:00
2021-07-15 09:50:11 -07:00
await update(user, { twoFactorAuthenticationSecret: secret.base32, twoFactorAuthenticationEnabled: false }, auditSource);
2018-04-25 19:08:15 +02:00
2021-07-15 09:50:11 -07:00
const [error, dataUrl] = await safe(qrcode.toDataURL(secret.otpauth_url));
if (error) throw new BoxError(BoxError.INTERNAL_ERROR, error);
2018-04-25 19:08:15 +02:00
2021-07-15 09:50:11 -07:00
return { secret: secret.base32, qrcode: dataUrl };
2018-04-25 19:08:15 +02:00
}
async function enableTwoFactorAuthentication(user, totpToken, auditSource) {
assert.strictEqual(typeof user, 'object');
2018-04-25 19:08:15 +02:00
assert.strictEqual(typeof totpToken, 'string');
2021-07-15 09:50:11 -07:00
assert(auditSource && typeof auditSource === 'object');
2018-04-25 19:08:15 +02:00
const externalLdapConfig = await externalLdap.getConfig();
if (user.source === 'ldap' && externalLdap.supports2FA(externalLdapConfig)) throw new BoxError(BoxError.BAD_STATE, 'Cannot enable 2FA of external auth user');
2018-04-25 19:08:15 +02:00
2021-07-15 09:50:11 -07:00
const verified = speakeasy.totp.verify({ secret: user.twoFactorAuthenticationSecret, encoding: 'base32', token: totpToken, window: 2 });
if (!verified) throw new BoxError(BoxError.INVALID_CREDENTIALS, 'Invalid 2FA code');
2018-04-25 19:08:15 +02:00
if (user.twoFactorAuthenticationEnabled) throw new BoxError(BoxError.ALREADY_EXISTS, '2FA already enabled');
2018-04-25 19:08:15 +02:00
2021-07-15 09:50:11 -07:00
await update(user, { twoFactorAuthenticationEnabled: true }, auditSource);
2018-04-25 19:08:15 +02:00
}
async function disableTwoFactorAuthentication(user, auditSource) {
assert.strictEqual(typeof user, 'object');
2021-07-15 09:50:11 -07:00
assert(auditSource && typeof auditSource === 'object');
2019-08-08 05:45:56 -07:00
const externalLdapConfig = await externalLdap.getConfig();
if (user.source === 'ldap' && externalLdap.supports2FA(externalLdapConfig)) throw new BoxError(BoxError.BAD_STATE, 'Cannot disable 2FA of external auth user');
2021-07-15 09:50:11 -07:00
await update(user, { twoFactorAuthenticationEnabled: false, twoFactorAuthenticationSecret: '' }, auditSource);
2019-08-08 05:45:56 -07:00
}
2020-01-31 15:28:42 -08:00
async function getAvatar(user) {
assert.strictEqual(typeof user, 'object');
const result = await database.query('SELECT avatar FROM users WHERE id = ?', [ user.id ]);
2021-07-07 14:31:39 +02:00
if (result.length === 0) throw new BoxError(BoxError.NOT_FOUND, 'User not found');
2021-06-25 22:11:17 -07:00
return result[0].avatar;
}
async function setAvatar(user, avatar) {
assert.strictEqual(typeof user, 'object');
assert(Buffer.isBuffer(avatar) || avatar === null);
const result = await database.query('UPDATE users SET avatar=? WHERE id = ?', [ avatar, user.id ]);
2021-06-25 22:11:17 -07:00
if (result.length === 0) throw new BoxError(BoxError.NOT_FOUND, 'User not found');
}
2022-05-14 19:41:32 +02:00
async function getBackgroundImage(user) {
assert.strictEqual(typeof user, 'object');
2022-05-14 19:41:32 +02:00
const result = await database.query('SELECT backgroundImage FROM users WHERE id = ?', [ user.id ]);
2022-05-14 19:41:32 +02:00
if (result.length === 0) throw new BoxError(BoxError.NOT_FOUND, 'User not found');
return result[0].backgroundImage;
}
async function setBackgroundImage(user, backgroundImage) {
assert.strictEqual(typeof user, 'object');
2022-05-15 12:14:17 +02:00
assert(Buffer.isBuffer(backgroundImage) || backgroundImage === null);
2022-05-14 19:41:32 +02:00
const result = await database.query('UPDATE users SET backgroundImage=? WHERE id = ?', [ backgroundImage, user.id ]);
2022-05-14 19:41:32 +02:00
if (result.length === 0) throw new BoxError(BoxError.NOT_FOUND, 'User not found');
}
2024-12-11 18:24:20 +01:00
async function setNotificationConfig(user, notificationConfig, auditSource) {
assert.strictEqual(typeof user, 'object');
assert(Array.isArray(notificationConfig));
assert(auditSource && typeof auditSource === 'object');
await update(user, { notificationConfig }, auditSource);
2024-12-11 18:24:20 +01:00
}
2024-12-04 09:48:25 +01:00
async function resetSources() {
await database.query('UPDATE users SET source = ?', [ '' ]);
}
function parseDisplayName(displayName) {
assert.strictEqual(typeof displayName, 'string');
const middleName = '';
const idx = displayName.indexOf(' ');
if (idx === -1) return { firstName: displayName, lastName: '', middleName };
const firstName = displayName.substring(0, idx);
const lastName = displayName.substring(idx+1);
return { firstName, lastName, middleName };
}