2015-07-20 00:09:47 -07:00
'use strict' ;
exports = module . exports = {
2021-04-14 15:54:09 -07:00
removePrivateFields ,
removeRestrictedFields ,
2021-07-15 09:50:11 -07:00
add ,
createOwner ,
isActivated ,
2021-04-14 15:54:09 -07:00
getAll ,
getAllPaged ,
get ,
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 ,
2021-04-19 20:52:10 -07:00
getSuperadmins ,
2021-07-15 09:50:11 -07:00
verify ,
verifyWithUsername ,
verifyWithEmail ,
2021-04-14 15:54:09 -07:00
setPassword ,
update ,
2021-07-15 09:50:11 -07:00
del ,
2021-04-14 15:54:09 -07:00
createInvite ,
sendInvite ,
2021-07-15 09:50:11 -07:00
2021-04-14 15:54:09 -07:00
setTwoFactorAuthenticationSecret ,
enableTwoFactorAuthentication ,
disableTwoFactorAuthentication ,
sendPasswordResetByIdentifier ,
2020-07-09 14:42:39 -07:00
2021-07-15 09:50:11 -07:00
notifyLoginLocation ,
2021-04-30 13:21:50 +02:00
2020-07-09 16:39:29 -07:00
setupAccount ,
2021-07-15 09:50:11 -07:00
2021-04-29 12:49:48 -07:00
getAvatarUrl ,
2020-07-09 22:33:36 -07:00
setAvatar ,
2021-04-29 12:49:48 -07:00
getAvatar ,
2020-07-09 16:39:29 -07:00
2020-01-31 15:28:42 -08:00
AP _MAIL : 'mail' ,
AP _WEBADMIN : 'webadmin' ,
2020-02-21 12:17:06 -08:00
ROLE _ADMIN : 'admin' ,
ROLE _USER : 'user' ,
ROLE _USER _MANAGER : 'usermanager' ,
ROLE _OWNER : 'owner' ,
2021-04-14 15:54:09 -07:00
compareRoles ,
2015-07-20 00:09:47 -07:00
} ;
2020-02-21 12:17:06 -08:00
const ORDERED _ROLES = [ exports . ROLE _USER , exports . ROLE _USER _MANAGER , exports . ROLE _ADMIN , exports . ROLE _OWNER ] ;
2021-07-15 09:50:11 -07:00
// the avatar field is special and not added here to reduce response sizes
const USERS _FIELDS = [ 'id' , 'username' , 'email' , 'fallbackEmail' , 'password' , 'salt' , 'creationTime' , 'resetToken' , 'displayName' ,
'twoFactorAuthenticationEnabled' , 'twoFactorAuthenticationSecret' , 'active' , 'source' , 'role' , 'resetTokenCreationTime' , 'loginLocationsJson' ] . join ( ',' ) ;
2021-06-25 22:11:17 -07:00
const appPasswords = require ( './apppasswords.js' ) ,
assert = require ( 'assert' ) ,
2019-10-22 16:34:17 -07:00
BoxError = require ( './boxerror.js' ) ,
2015-07-20 00:09:47 -07:00
crypto = require ( 'crypto' ) ,
2016-07-12 10:07:55 -07:00
constants = require ( './constants.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' ) ,
2018-06-11 12:38:15 -07:00
hat = require ( './hat.js' ) ,
2016-02-08 15:15:42 -08:00
mailer = require ( './mailer.js' ) ,
2021-07-15 09:50:11 -07:00
mysql = require ( 'mysql' ) ,
2020-03-15 11:41:39 -07:00
paths = require ( './paths.js' ) ,
2018-04-25 19:08:15 +02:00
qrcode = require ( 'qrcode' ) ,
2016-07-12 10:07:55 -07:00
safe = require ( 'safetydance' ) ,
2019-07-26 10:49:29 -07:00
settings = require ( './settings.js' ) ,
2018-04-25 19:08:15 +02:00
speakeasy = require ( 'speakeasy' ) ,
2020-07-09 16:39:29 -07:00
tokens = require ( './tokens.js' ) ,
2017-08-13 17:44:31 -07:00
uuid = require ( 'uuid' ) ,
2021-05-04 09:11:16 +02:00
uaParser = require ( 'ua-parser-js' ) ,
2021-04-30 13:21:50 +02:00
superagent = require ( 'superagent' ) ,
2021-07-15 09:50:11 -07:00
util = require ( 'util' ) ,
2015-07-20 00:09:47 -07:00
validator = require ( 'validator' ) ,
_ = require ( 'underscore' ) ;
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
2015-07-20 00:09:47 -07:00
2021-07-15 09:50:11 -07:00
const pbkdf2Async = util . promisify ( crypto . pbkdf2 ) ;
const getDirectoryConfigAsync = util . promisify ( settings . getDirectoryConfig ) ;
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 ;
return result ;
}
2018-01-25 18:03:26 +01:00
// keep this in sync with validateGroupname and validateAlias
2015-07-20 00:09:47 -07:00
function validateUsername ( username ) {
assert . strictEqual ( typeof username , 'string' ) ;
2016-04-01 16:48:34 +02:00
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' ) ;
2015-07-20 00:09:47 -07:00
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
2016-05-30 01:32:18 -07:00
// app emails are sent using the .app suffix
2019-10-24 14:40:26 -07:00
if ( username . indexOf ( '.app' ) !== - 1 ) return new BoxError ( BoxError . BAD _FIELD , 'Username pattern is reserved for apps' ) ;
2016-05-18 21:45:02 -07:00
2015-07-20 00:09:47 -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' ) ;
2015-07-20 00:09:47 -07:00
return null ;
}
2021-07-15 09:50:11 -07:00
function validateResetToken ( token ) {
2015-07-20 00:09:47 -07:00
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
2015-07-20 00:09:47 -07:00
return null ;
}
2016-01-19 23:34:49 -08:00
function validateDisplayName ( name ) {
assert . strictEqual ( typeof name , 'string' ) ;
return null ;
}
2018-06-11 12:55:24 -07:00
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' ) ;
2018-06-11 12:55:24 -07:00
return null ;
}
2018-06-25 15:54:24 -07:00
// remove all fields that should never be sent out via REST API
2018-03-02 11:24:06 +01:00
function removePrivateFields ( user ) {
2021-04-14 21:46:35 -07:00
return _ . pick ( user , 'id' , 'username' , 'email' , 'fallbackEmail' , 'displayName' , 'groupIds' , 'active' , 'source' , 'role' , 'createdAt' , 'twoFactorAuthenticationEnabled' ) ;
2018-03-02 11:24:06 +01:00
}
2018-06-25 15:54:24 -07:00
// remove all fields that Non-privileged users must not see
function removeRestrictedFields ( user ) {
2019-08-08 07:19:50 -07:00
return _ . pick ( user , 'id' , 'username' , 'email' , 'displayName' , 'active' ) ;
2018-06-25 15:54:24 -07:00
}
2021-07-15 09:50:11 -07:00
async function add ( email , data , auditSource ) {
2015-07-20 00:09:47 -07:00
assert . strictEqual ( typeof email , 'string' ) ;
2021-07-15 09:50:11 -07:00
assert ( data && typeof data === 'object' ) ;
2019-01-23 11:18:31 +01:00
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' ) ;
let { username , password , displayName } = data ;
const source = data . source || '' ; // empty is local user
const role = data . role || exports . ROLE _USER ;
2015-07-20 00:09:47 -07:00
2021-05-17 07:18:21 -07:00
let error ;
2016-04-14 16:25:46 +02:00
2017-02-02 00:23:11 -08:00
if ( username !== null ) {
username = username . toLowerCase ( ) ;
error = validateUsername ( username ) ;
2021-07-15 09:50:11 -07:00
if ( error ) throw error ;
2017-02-02 00:23:11 -08:00
}
2015-07-20 00:09:47 -07:00
2018-06-11 12:59:52 -07:00
if ( password !== null ) {
error = validatePassword ( password ) ;
2021-07-15 09:50:11 -07:00
if ( error ) throw error ;
2018-06-11 12:59:52 -07:00
} else {
password = hat ( 8 * 8 ) ;
}
2015-07-20 00:09:47 -07:00
2017-02-02 00:23:11 -08:00
email = email . toLowerCase ( ) ;
2015-07-20 00:09:47 -07:00
error = validateEmail ( email ) ;
2021-07-15 09:50:11 -07:00
if ( error ) throw error ;
2015-07-20 00:09:47 -07:00
2016-01-19 23:34:49 -08:00
error = validateDisplayName ( displayName ) ;
2021-07-15 09:50:11 -07:00
if ( error ) throw error ;
2016-01-19 23:34:49 -08:00
2020-02-21 12:17:06 -08:00
error = validateRole ( role ) ;
2021-07-15 09:50:11 -07:00
if ( error ) throw error ;
const randomBytes = util . promisify ( crypto . randomBytes ) ;
let salt , derivedKey ;
[ error , salt ] = await safe ( randomBytes ( CRYPTO _SALT _SIZE ) ) ;
if ( error ) throw new BoxError ( BoxError . CRYPTO _ERROR , error ) ;
[ error , derivedKey ] = await safe ( pbkdf2Async ( password , salt , CRYPTO _ITERATIONS , CRYPTO _KEY _LENGTH , CRYPTO _DIGEST ) ) ;
if ( error ) throw new BoxError ( BoxError . CRYPTO _ERROR , error ) ;
const user = {
id : 'uid-' + uuid . v4 ( ) ,
username : username ,
email : email ,
fallbackEmail : email ,
password : Buffer . from ( derivedKey , 'binary' ) . toString ( 'hex' ) ,
salt : salt . toString ( 'hex' ) ,
resetToken : '' ,
displayName : displayName ,
source : source ,
role : role ,
avatar : constants . AVATAR _NONE
} ;
const query = 'INSERT INTO users (id, username, password, email, fallbackEmail, salt, resetToken, displayName, source, role, avatar) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)' ;
const args = [ user . id , user . username , user . password , user . email , user . fallbackEmail , user . salt , user . resetToken , user . displayName , user . source , user . role , user . avatar ] ;
[ error ] = await safe ( database . query ( query , args ) ) ;
if ( error && error . code === 'ER_DUP_ENTRY' && error . sqlMessage . indexOf ( 'users_email' ) !== - 1 ) throw new BoxError ( BoxError . ALREADY _EXISTS , 'email already exists' ) ;
if ( error && error . code === 'ER_DUP_ENTRY' && error . sqlMessage . indexOf ( 'users_username' ) !== - 1 ) throw new BoxError ( BoxError . ALREADY _EXISTS , 'username already exists' ) ;
if ( error && error . code === 'ER_DUP_ENTRY' && error . sqlMessage . indexOf ( 'PRIMARY' ) !== - 1 ) throw new BoxError ( BoxError . ALREADY _EXISTS , 'id already exists' ) ;
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 ;
eventlog . add ( eventlog . ACTION _USER _ADD , auditSource , { userId : user . id , email : user . email , user : removePrivateFields ( user ) } ) ;
2021-07-19 12:43:30 -07:00
return user . id ;
2015-07-20 00:09:47 -07:00
}
2016-07-12 10:07:55 -07:00
// returns true if ghost user was matched
function verifyGhost ( username , password ) {
assert . strictEqual ( typeof username , 'string' ) ;
assert . strictEqual ( typeof password , 'string' ) ;
2020-03-15 11:41:39 -07:00
var ghostData = safe . JSON . parse ( safe . fs . readFileSync ( paths . GHOST _USER _FILE , 'utf8' ) ) ;
2016-07-12 10:07:55 -07:00
if ( ! ghostData ) return false ;
if ( username in ghostData && ghostData [ username ] === password ) {
debug ( 'verifyGhost: matched ghost user' ) ;
2020-09-17 10:46:15 -07:00
safe . fs . unlinkSync ( paths . GHOST _USER _FILE ) ;
2016-07-12 10:07:55 -07:00
return true ;
}
return false ;
}
2021-07-15 09:50:11 -07:00
async function verifyAppPassword ( userId , password , identifier ) {
2020-01-31 15:28:42 -08:00
assert . strictEqual ( typeof userId , 'string' ) ;
assert . strictEqual ( typeof password , 'string' ) ;
assert . strictEqual ( typeof identifier , 'string' ) ;
2021-07-15 09:50:11 -07:00
const results = await appPasswords . list ( userId ) ;
2020-01-31 15:28:42 -08:00
2021-06-25 22:11:17 -07:00
const hashedPasswords = results . filter ( r => r . identifier === identifier ) . map ( r => r . hashedPassword ) ;
let hash = crypto . createHash ( 'sha256' ) . update ( password ) . digest ( 'base64' ) ;
2020-01-31 15:28:42 -08:00
2021-07-15 09:50:11 -07:00
if ( hashedPasswords . includes ( hash ) ) return ;
2020-01-31 15:28:42 -08:00
2021-07-15 09:50:11 -07:00
throw new BoxError ( BoxError . INVALID _CREDENTIALS ) ;
2020-01-31 15:28:42 -08:00
}
2021-07-15 09:50:11 -07:00
async function verify ( userId , password , identifier ) {
2016-04-05 16:27:04 +02:00
assert . strictEqual ( typeof userId , 'string' ) ;
assert . strictEqual ( typeof password , 'string' ) ;
2020-01-31 15:28:42 -08:00
assert . strictEqual ( typeof identifier , 'string' ) ;
2016-04-05 16:27:04 +02:00
2021-07-15 09:50:11 -07:00
const user = await get ( userId ) ;
if ( ! user ) throw new BoxError ( BoxError . NOT _FOUND , 'User not found' ) ;
if ( ! user . active ) throw new BoxError ( BoxError . NOT _FOUND , 'User not active' ) ;
2016-04-05 16:27:04 +02:00
2021-07-15 09:50:11 -07:00
// for just invited users the username may be still null
if ( user . username && verifyGhost ( user . username , password ) ) {
user . ghost = true ;
return user ;
}
2019-08-08 05:45:56 -07:00
2021-07-15 09:50:11 -07:00
const [ error ] = await safe ( verifyAppPassword ( user . id , password , identifier ) ) ;
if ( ! error ) { // matched app password
user . appPassword = true ;
return user ;
}
2016-07-12 10:07:55 -07:00
2021-07-15 09:50:11 -07:00
if ( user . source === 'ldap' ) {
await externalLdap . verifyPassword ( user , password ) ;
} 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 ) throw new BoxError ( BoxError . INVALID _CREDENTIALS ) ;
}
return user ;
2016-04-05 16:27:04 +02:00
}
2021-07-15 09:50:11 -07:00
async function verifyWithUsername ( username , password , identifier ) {
2015-07-20 00:09:47 -07:00
assert . strictEqual ( typeof username , 'string' ) ;
assert . strictEqual ( typeof password , 'string' ) ;
2020-01-31 15:28:42 -08:00
assert . strictEqual ( typeof identifier , 'string' ) ;
2015-07-20 00:09:47 -07:00
2021-07-15 09:50:11 -07:00
const user = await getByUsername ( username . toLowerCase ( ) ) ;
if ( ! user ) throw new BoxError ( BoxError . NOT _FOUND , 'User not found' ) ;
2015-07-20 00:09:47 -07:00
2021-07-15 09:50:11 -07:00
return await verify ( user . id , password , identifier ) ;
2015-07-20 00:09:47 -07:00
}
2021-07-15 09:50:11 -07:00
async function verifyWithEmail ( email , password , identifier ) {
2015-07-20 00:09:47 -07:00
assert . strictEqual ( typeof email , 'string' ) ;
assert . strictEqual ( typeof password , 'string' ) ;
2020-01-31 15:28:42 -08:00
assert . strictEqual ( typeof identifier , 'string' ) ;
2015-07-20 00:09:47 -07:00
2021-07-15 09:50:11 -07:00
const user = await getByEmail ( email . toLowerCase ( ) ) ;
if ( ! user ) throw new BoxError ( BoxError . NOT _FOUND , 'User not found' ) ;
2016-09-27 15:41:34 +02:00
2021-07-15 09:50:11 -07:00
return await verify ( user . id , password , identifier ) ;
2015-07-20 00:09:47 -07:00
}
2021-06-26 09:57:07 -07:00
async function del ( user , auditSource ) {
2020-02-13 20:45:00 -08:00
assert . strictEqual ( typeof user , 'object' ) ;
2019-01-23 11:18:31 +01:00
assert ( auditSource && typeof auditSource === 'object' ) ;
2015-07-20 00:09:47 -07:00
2021-06-26 09:57:07 -07:00
if ( settings . isDemo ( ) && user . username === constants . DEMO _USERNAME ) throw new BoxError ( BoxError . BAD _FIELD , 'Not allowed in demo mode' ) ;
2015-07-20 00:09:47 -07:00
2021-06-26 09:57:07 -07:00
const queries = [ ] ;
queries . push ( { query : 'DELETE FROM groupMembers WHERE userId = ?' , args : [ user . id ] } ) ;
queries . push ( { query : 'DELETE FROM tokens WHERE identifier = ?' , args : [ user . id ] } ) ;
queries . push ( { query : 'DELETE FROM appPasswords WHERE userId = ?' , args : [ user . id ] } ) ;
queries . push ( { query : 'DELETE FROM users WHERE id = ?' , args : [ user . id ] } ) ;
2016-05-01 20:09:31 -07:00
2021-06-26 09:57:07 -07:00
const [ error , result ] = await safe ( database . transaction ( queries ) ) ;
if ( error && error . code === 'ER_NO_REFERENCED_ROW_2' ) throw new BoxError ( BoxError . NOT _FOUND , error ) ;
if ( error ) throw error ;
if ( result [ 3 ] . affectedRows !== 1 ) throw new BoxError ( BoxError . NOT _FOUND , 'User not found' ) ;
2021-06-03 11:42:32 -07:00
2021-06-26 09:57:07 -07:00
await safe ( eventlog . add ( eventlog . ACTION _USER _REMOVE , auditSource , { userId : user . id , user : removePrivateFields ( user ) } ) ) ;
2015-07-20 00:09:47 -07:00
}
2021-07-15 09:50:11 -07:00
async function getAll ( ) {
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
}
2021-07-15 09:50:11 -07:00
async function getAllPaged ( search , page , perPage ) {
2019-01-15 17:21:40 +01:00
assert ( typeof search === 'string' || search === 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 ' ;
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 ( ) } % ` ) + ')' ;
}
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 ) ;
2018-11-10 18:08:08 -08:00
2021-07-15 09:50:11 -07:00
return results ;
}
2018-11-10 18:08:08 -08:00
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 ;
2018-11-10 18:08:08 -08:00
}
2021-07-15 09:50:11 -07:00
async function get ( userId ) {
2015-07-20 00:09:47 -07:00
assert . strictEqual ( typeof userId , '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.id = ?' , [ userId ] ) ;
2015-07-20 00:09:47 -07:00
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 getByEmail ( email ) {
assert . strictEqual ( typeof email , 'string' ) ;
const result = await database . query ( ` SELECT ${ USERS _FIELDS } FROM users WHERE email = ? ` , [ email ] ) ;
if ( result . length === 0 ) return null ;
return postProcess ( result [ 0 ] ) ;
2015-07-20 00:09:47 -07:00
}
2021-07-15 09:50:11 -07:00
async function getByRole ( 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 } FROM users WHERE role=? ORDER BY creationTime ` , [ role ] ) ;
results . forEach ( postProcess ) ;
return results ;
}
async function getByResetToken ( resetToken ) {
2015-07-20 00:09:47 -07:00
assert . strictEqual ( typeof resetToken , 'string' ) ;
2021-07-15 09:50:11 -07:00
let error = validateResetToken ( resetToken ) ;
if ( error ) throw error ;
2015-07-20 00:09:47 -07:00
2021-07-15 09:50:11 -07:00
const result = await database . query ( ` SELECT ${ USERS _FIELDS } FROM users WHERE resetToken=? ` , [ resetToken ] ) ;
if ( result . length === 0 ) return null ;
2015-07-20 00:09:47 -07:00
2021-07-15 09:50:11 -07:00
return postProcess ( result [ 0 ] ) ;
2015-07-20 00:09:47 -07:00
}
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' ) ;
2021-07-15 09:50:11 -07:00
const result = await database . query ( ` SELECT ${ USERS _FIELDS } FROM users WHERE username = ? ` , [ username ] ) ;
if ( result . length === 0 ) return null ;
2019-03-18 21:15:50 -07:00
2021-07-15 09:50:11 -07:00
return postProcess ( result [ 0 ] ) ;
2019-03-18 21:15:50 -07:00
}
2021-07-15 09:50:11 -07:00
async function update ( user , data , auditSource ) {
2020-02-13 20:45:00 -08:00
assert . strictEqual ( typeof user , 'object' ) ;
2016-06-02 23:53:06 -07:00
assert . strictEqual ( typeof data , 'object' ) ;
2019-01-23 11:18:31 +01:00
assert ( auditSource && typeof auditSource === 'object' ) ;
2015-07-20 00:09:47 -07:00
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 ) ) ) ;
2020-10-23 11:41:39 -07:00
2021-07-15 09:50:11 -07:00
if ( settings . isDemo ( ) && user . username === constants . DEMO _USERNAME ) throw new BoxError ( BoxError . BAD _FIELD , 'Not allowed in demo mode' ) ;
let error , result ;
2016-04-14 16:25:46 +02:00
2021-07-15 09:50:11 -07:00
if ( _ . isEmpty ( data ) ) return ;
2015-07-20 00:09:47 -07:00
2016-06-02 23:53:06 -07:00
if ( data . username ) {
data . username = data . username . toLowerCase ( ) ;
error = validateUsername ( data . username ) ;
2021-07-15 09:50:11 -07:00
if ( error ) throw error ;
2016-06-02 23:53:06 -07:00
}
if ( data . email ) {
data . email = data . email . toLowerCase ( ) ;
error = validateEmail ( data . email ) ;
2021-07-15 09:50:11 -07:00
if ( error ) throw error ;
2016-06-02 23:53:06 -07:00
}
2015-07-20 00:09:47 -07:00
2018-01-21 14:25:39 +01:00
if ( data . fallbackEmail ) {
data . fallbackEmail = data . fallbackEmail . toLowerCase ( ) ;
error = validateEmail ( data . fallbackEmail ) ;
2021-07-15 09:50:11 -07:00
if ( error ) throw error ;
2018-01-21 14:25:39 +01:00
}
2020-02-21 12:17:06 -08:00
if ( data . role ) {
error = validateRole ( data . role ) ;
2021-07-15 09:50:11 -07:00
if ( error ) throw error ;
2020-02-13 22:06:54 -08:00
}
2021-07-15 09:50:11 -07:00
let args = [ ] ;
let fields = [ ] ;
for ( const k in data ) {
if ( k === 'twoFactorAuthenticationEnabled' || k === 'active' ) {
fields . push ( k + ' = ?' ) ;
args . push ( data [ k ] ? 1 : 0 ) ;
} else if ( k === 'loginLocations' ) {
fields . push ( 'loginLocationsJson = ?' ) ;
args . push ( JSON . stringify ( data [ k ] ) ) ;
} else {
fields . push ( k + ' = ?' ) ;
args . push ( data [ k ] ) ;
}
}
args . push ( user . id ) ;
2016-05-01 20:09:31 -07:00
2021-07-15 09:50:11 -07:00
[ error , result ] = await safe ( database . query ( 'UPDATE users SET ' + fields . join ( ', ' ) + ' WHERE id = ?' , args ) ) ;
if ( error && error . code === 'ER_DUP_ENTRY' && error . sqlMessage . indexOf ( 'users_email' ) !== - 1 ) throw new BoxError ( BoxError . ALREADY _EXISTS , 'email already exists' ) ;
if ( error && error . code === 'ER_DUP_ENTRY' && error . sqlMessage . indexOf ( 'users_username' ) !== - 1 ) throw new BoxError ( BoxError . ALREADY _EXISTS , 'username already exists' ) ;
if ( error ) throw new BoxError ( BoxError . DATABASE _ERROR , error ) ;
if ( result . affectedRows !== 1 ) throw new BoxError ( BoxError . NOT _FOUND , 'User not found' ) ;
2020-02-13 20:45:00 -08:00
2021-07-15 09:50:11 -07:00
const newUser = _ . extend ( { } , user , data ) ;
2020-02-14 13:01:51 -08:00
2021-07-15 09:50:11 -07:00
eventlog . add ( eventlog . ACTION _USER _UPDATE , auditSource , {
userId : user . id ,
user : removePrivateFields ( newUser ) ,
roleChanged : newUser . role !== user . role ,
activeStatusChanged : ( ( newUser . active && ! user . active ) || ( ! newUser . active && user . active ) )
2016-09-21 15:34:58 -07:00
} ) ;
2015-07-20 00:09:47 -07:00
}
2021-07-15 09:50:11 -07:00
async function getOwner ( ) {
const owners = await getByRole ( exports . ROLE _OWNER ) ;
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 getByRole ( exports . ROLE _OWNER ) ;
const admins = await getByRole ( exports . ROLE _ADMIN ) ;
2020-02-21 12:17:06 -08:00
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 getByRole ( exports . ROLE _OWNER ) ;
2021-04-19 20:52:10 -07:00
}
2021-07-15 09:50:11 -07:00
async function sendPasswordResetByIdentifier ( identifier , auditSource ) {
2015-07-20 00:09:47 -07:00
assert . strictEqual ( typeof identifier , 'string' ) ;
2021-07-15 09:50:11 -07:00
assert . strictEqual ( typeof auditSource , 'object' ) ;
2015-07-20 00:09:47 -07:00
2021-07-15 09:50:11 -07:00
const user = identifier . indexOf ( '@' ) === - 1 ? await getByUsername ( identifier . toLowerCase ( ) ) : await getByEmail ( identifier . toLowerCase ( ) ) ;
if ( ! user ) throw new BoxError ( BoxError . NOT _FOUND , 'User not found' ) ;
2015-07-20 00:09:47 -07:00
2021-07-15 09:50:11 -07:00
let resetToken = hat ( 256 ) , resetTokenCreationTime = new Date ( ) ;
user . resetToken = resetToken ;
user . resetTokenCreationTime = resetTokenCreationTime ;
2015-07-20 00:09:47 -07:00
2021-07-15 09:50:11 -07:00
await update ( user , { resetToken , resetTokenCreationTime } , auditSource ) ;
2015-07-20 00:09:47 -07:00
2021-07-15 09:50:11 -07:00
mailer . passwordReset ( user ) ;
2015-07-20 00:09:47 -07:00
}
2021-07-15 09:50:11 -07:00
async function notifyLoginLocation ( user , ip , userAgent , auditSource ) {
2021-04-30 13:21:50 +02:00
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' ) ;
2021-04-30 13:21:50 +02:00
2021-07-15 09:50:11 -07:00
debug ( ` notifyLoginLocation: ${ user . id } ${ ip } ${ userAgent } ` ) ;
2021-04-30 13:21:50 +02:00
2021-07-15 09:50:11 -07:00
if ( constants . TEST && ip === '127.0.0.1' ) return ;
2021-04-30 13:21:50 +02:00
2021-07-15 09:50:11 -07:00
const response = await superagent . get ( 'https://geolocation.cloudron.io/json' ) . query ( { ip } ) . ok ( ( ) => true ) . end ( ) ;
if ( response . statusCode !== 200 ) return console . error ( ` Failed to get geoip info. statusCode: ${ response . statusCode } ` ) ;
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-04-30 13:21:50 +02:00
2021-07-15 09:50:11 -07:00
if ( ! city || ! country ) return ;
2021-05-04 09:11:16 +02:00
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-04-30 13:21:50 +02:00
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-04-30 13:21:50 +02:00
2021-07-15 09:50:11 -07:00
if ( knownLogin ) return ;
2021-04-30 13:21:50 +02:00
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 } ;
let loginLocations = user . loginLocations . filter ( function ( l ) { return l . ts > sixMonthsBack ; } ) ;
2021-05-04 20:02:53 +02:00
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-04-30 13:21:50 +02:00
2021-07-15 09:50:11 -07:00
await update ( user , { loginLocations } , auditSource ) ;
mailer . sendNewLoginLocation ( user , newLoginLocation ) ;
2021-04-30 13:21:50 +02:00
}
2021-07-15 09:50:11 -07:00
async function setPassword ( user , newPassword , auditSource ) {
2020-02-13 20:45:00 -08:00
assert . strictEqual ( typeof user , 'object' ) ;
2015-07-20 00:09:47 -07:00
assert . strictEqual ( typeof newPassword , 'string' ) ;
2021-07-15 09:50:11 -07:00
assert . strictEqual ( typeof auditSource , 'object' ) ;
2015-07-20 00:09:47 -07:00
2021-07-15 09:50:11 -07:00
let error = validatePassword ( newPassword ) ;
if ( error ) throw error ;
2015-07-20 00:09:47 -07:00
2021-07-15 09:50:11 -07:00
if ( settings . isDemo ( ) && user . username === constants . DEMO _USERNAME ) throw new BoxError ( BoxError . BAD _FIELD , 'Not allowed in demo mode' ) ;
if ( user . source ) throw new BoxError ( BoxError . CONFLICT , 'User is from an external directory' ) ;
2015-07-20 00:09:47 -07:00
2021-07-15 09:50:11 -07:00
const saltBuffer = Buffer . from ( user . salt , 'hex' ) ;
2015-07-20 00:09:47 -07:00
2021-07-15 09:50:11 -07:00
let derivedKey ;
[ error , derivedKey ] = await safe ( pbkdf2Async ( newPassword , saltBuffer , CRYPTO _ITERATIONS , CRYPTO _KEY _LENGTH , CRYPTO _DIGEST ) ) ;
if ( error ) throw new BoxError ( BoxError . CRYPTO _ERROR , error ) ;
2015-07-20 00:09:47 -07:00
2021-07-15 09:50:11 -07:00
const data = {
password : Buffer . from ( derivedKey , 'binary' ) . toString ( 'hex' ) ,
resetToken : ''
} ;
2015-07-20 00:09:47 -07:00
2021-07-15 09:50:11 -07:00
await update ( user , data , auditSource ) ;
2015-07-20 00:09:47 -07:00
}
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' ) ;
2019-01-23 11:18:31 +01:00
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' ) ;
2016-04-04 15:16:16 +02:00
2021-07-15 09:50:11 -07:00
const activated = await isActivated ( ) ;
if ( activated ) throw new BoxError ( BoxError . ALREADY _EXISTS , 'Cloudron already activated' ) ;
2015-07-20 00:09:47 -07:00
2021-07-15 09:50:11 -07:00
return await add ( email , { username , password , displayName , role : exports . ROLE _OWNER } , auditSource ) ;
2016-01-13 12:28:38 -08:00
}
2016-01-18 15:16:18 +01:00
2020-07-09 16:39:29 -07:00
function inviteLink ( user , directoryConfig ) {
2021-05-05 12:29:04 -07:00
let link = ` ${ settings . dashboardOrigin ( ) } /setupaccount.html?resetToken= ${ user . resetToken } &email= ${ encodeURIComponent ( user . email ) } ` ;
2020-02-05 16:34:34 +01:00
if ( user . username ) link += ` &username= ${ encodeURIComponent ( user . username ) } ` ;
if ( user . displayName ) link += ` &displayName= ${ encodeURIComponent ( user . displayName ) } ` ;
2020-07-09 16:39:29 -07:00
if ( directoryConfig . lockUserProfiles ) link += '&profileLocked=true' ;
2020-02-05 16:34:34 +01:00
return link ;
2020-02-05 15:53:05 +01:00
}
2021-07-15 09:50:11 -07:00
async function createInvite ( user , auditSource ) {
2020-02-13 20:45:00 -08:00
assert . strictEqual ( typeof user , 'object' ) ;
2021-07-15 09:50:11 -07: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' ) ;
2019-10-29 11:03:28 -07:00
2020-04-08 13:11:24 -07:00
const resetToken = hat ( 256 ) , resetTokenCreationTime = new Date ( ) ;
2016-01-18 15:16:18 +01:00
2021-07-15 09:50:11 -07:00
const directoryConfig = await getDirectoryConfigAsync ( ) ;
2016-01-18 15:16:18 +01:00
2021-07-15 09:50:11 -07:00
await update ( user , { resetToken , resetTokenCreationTime } , auditSource ) ;
2020-07-09 16:39:29 -07:00
2021-07-15 09:50:11 -07:00
user . resetToken = resetToken ;
2020-07-09 16:39:29 -07:00
2021-07-15 09:50:11 -07:00
return { resetToken , inviteLink : inviteLink ( user , directoryConfig ) } ;
2016-01-18 15:16:18 +01:00
}
2016-05-06 13:56:26 +02:00
2021-07-15 09:50:11 -07:00
async function sendInvite ( user , options ) {
2020-02-13 20:45:00 -08:00
assert . strictEqual ( typeof user , 'object' ) ;
2018-08-17 09:49:58 -07:00
assert . strictEqual ( typeof options , 'object' ) ;
2021-07-15 09:50:11 -07:00
if ( user . source ) throw new BoxError ( BoxError . CONFLICT , 'User is from an external directory' ) ;
if ( ! user . resetToken ) throw new BoxError ( BoxError . CONFLICT , 'Must generate resetToken to send invitation' ) ;
2018-08-17 09:49:58 -07:00
2021-07-15 09:50:11 -07:00
const directoryConfig = await getDirectoryConfigAsync ( ) ;
2020-07-09 16:39:29 -07:00
2021-07-15 09:50:11 -07:00
mailer . sendInvite ( user , options . invitor || null , inviteLink ( user , directoryConfig ) ) ;
2020-07-09 16:39:29 -07:00
}
2021-07-15 09:50:11 -07:00
async function setupAccount ( user , data , auditSource ) {
2020-07-09 16:39:29 -07:00
assert . strictEqual ( typeof user , 'object' ) ;
assert . strictEqual ( typeof data , 'object' ) ;
assert ( auditSource && typeof auditSource === 'object' ) ;
2021-07-15 09:50:11 -07:00
const directoryConfig = await getDirectoryConfigAsync ( ) ;
if ( directoryConfig . lockUserProfiles ) return ;
2018-08-17 09:49:58 -07:00
2021-07-15 09:50:11 -07:00
await update ( user , _ . pick ( data , 'username' , 'displayName' ) , auditSource ) ;
2020-07-09 16:39:29 -07:00
2021-07-15 09:50:11 -07:00
await setPassword ( user , data . password ) ; // setPassword clears the resetToken
2020-07-09 16:39:29 -07:00
2021-07-15 09:50:11 -07:00
const token = { clientId : tokens . ID _WEBADMIN , identifier : user . id , expires : Date . now ( ) + constants . DEFAULT _TOKEN _EXPIRATION _MSECS } ;
const result = await tokens . add ( token ) ;
return result . accessToken ;
2018-08-17 09:49:58 -07:00
}
2021-07-15 09:50:11 -07:00
async function setTwoFactorAuthenticationSecret ( userId , auditSource ) {
2018-04-25 19:08:15 +02:00
assert . strictEqual ( typeof userId , 'string' ) ;
2021-07-15 09:50:11 -07:00
assert ( auditSource && typeof auditSource === 'object' ) ;
2018-04-25 19:08:15 +02:00
2021-07-15 09:50:11 -07:00
const user = await get ( userId ) ;
if ( ! user ) throw new BoxError ( BoxError . NOT _FOUND , 'User not found' ) ;
2018-04-25 19:08:15 +02:00
2021-07-15 09:50:11 -07:00
if ( settings . isDemo ( ) && user . username === constants . DEMO _USERNAME ) throw new BoxError ( BoxError . BAD _FIELD , 'Not allowed in demo mode' ) ;
2020-07-22 16:18:22 -07:00
2021-07-15 09:50:11 -07:00
if ( user . twoFactorAuthenticationEnabled ) throw new BoxError ( BoxError . ALREADY _EXISTS ) ;
2018-04-25 19:08:15 +02:00
2021-07-15 09:50:11 -07:00
const secret = speakeasy . generateSecret ( { name : ` Cloudron ${ settings . 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
}
2021-07-15 09:50:11 -07:00
async function enableTwoFactorAuthentication ( userId , totpToken , auditSource ) {
2018-04-25 19:08:15 +02:00
assert . strictEqual ( typeof userId , 'string' ) ;
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
2021-07-15 09:50:11 -07:00
const user = await get ( userId ) ;
if ( ! user ) throw new BoxError ( BoxError . NOT _FOUND , 'User not found' ) ;
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 ) ;
2018-04-25 19:08:15 +02:00
2021-07-15 09:50:11 -07:00
if ( user . twoFactorAuthenticationEnabled ) throw new BoxError ( BoxError . ALREADY _EXISTS ) ;
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
}
2021-07-15 09:50:11 -07:00
async function disableTwoFactorAuthentication ( userId , auditSource ) {
2018-04-25 19:08:15 +02:00
assert . strictEqual ( typeof userId , 'string' ) ;
2021-07-15 09:50:11 -07:00
assert ( auditSource && typeof auditSource === 'object' ) ;
2019-08-08 05:45:56 -07:00
2021-07-15 09:50:11 -07:00
const user = await get ( userId ) ;
if ( ! user ) throw new BoxError ( BoxError . NOT _FOUND , 'User not found' ) ;
await update ( user , { twoFactorAuthenticationEnabled : false , twoFactorAuthenticationSecret : '' } , auditSource ) ;
2019-08-08 05:45:56 -07:00
}
2020-01-31 15:28:42 -08:00
2020-02-21 12:17:06 -08:00
function validateRole ( role ) {
assert . strictEqual ( typeof role , 'string' ) ;
if ( ORDERED _ROLES . indexOf ( role ) !== - 1 ) 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' ) ;
let roleInt1 = ORDERED _ROLES . indexOf ( role1 ) ;
let roleInt2 = ORDERED _ROLES . indexOf ( role2 ) ;
return roleInt1 - roleInt2 ;
}
2021-06-25 22:11:17 -07:00
async function getAvatarUrl ( user ) {
2020-07-09 22:33:36 -07:00
assert . strictEqual ( typeof user , 'object' ) ;
2021-07-07 14:31:39 +02:00
const fallbackUrl = ` ${ settings . dashboardOrigin ( ) } /img/avatar-default-symbolic.svg ` ;
2020-07-09 22:33:36 -07:00
2021-07-08 08:52:51 -07:00
const result = await getAvatar ( user . id ) ;
2021-07-08 10:40:10 +02:00
if ( result . equals ( constants . AVATAR _NONE ) ) return fallbackUrl ;
else if ( result . equals ( constants . AVATAR _GRAVATAR ) ) return ` https://www.gravatar.com/avatar/ ${ require ( 'crypto' ) . createHash ( 'md5' ) . update ( user . email ) . digest ( 'hex' ) } .jpg ` ;
2021-07-07 14:31:39 +02:00
else if ( result ) return ` ${ settings . dashboardOrigin ( ) } /api/v1/profile/avatar/ ${ user . id } ` ;
else return fallbackUrl ;
2020-07-09 22:33:36 -07:00
}
2021-06-25 22:11:17 -07:00
async function getAvatar ( id ) {
2020-07-09 22:33:36 -07:00
assert . strictEqual ( typeof id , 'string' ) ;
2021-06-25 22:11:17 -07:00
const result = await database . query ( 'SELECT avatar FROM users WHERE id = ?' , [ 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 ;
2020-07-09 22:33:36 -07:00
}
2021-06-25 22:11:17 -07:00
async function setAvatar ( id , avatar ) {
2020-07-09 22:33:36 -07:00
assert . strictEqual ( typeof id , 'string' ) ;
2021-07-08 10:40:10 +02:00
assert ( Buffer . isBuffer ( avatar ) ) ;
2020-07-09 22:33:36 -07:00
2021-06-25 22:11:17 -07:00
const result = await database . query ( 'UPDATE users SET avatar=? WHERE id = ?' , [ avatar , id ] ) ;
if ( result . length === 0 ) throw new BoxError ( BoxError . NOT _FOUND , 'User not found' ) ;
2020-07-09 22:33:36 -07:00
}