2015-07-20 00:09:47 -07:00
'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 ,
2021-10-01 12:27:22 +02:00
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 ,
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 ,
2021-09-17 12:52:41 +02:00
setGhost ,
2024-01-20 10:41:24 +01:00
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 ,
2020-07-09 14:42:39 -07:00
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 ,
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
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
2022-05-14 19:41:32 +02:00
getBackgroundImage ,
setBackgroundImage ,
2023-08-03 08:11:42 +05:30
getProfileConfig ,
setProfileConfig ,
2024-01-13 11:27:08 +01:00
resetSource ,
2024-02-06 16:43:05 +01:00
parseDisplayName ,
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' ,
2021-12-01 09:27:24 -08:00
ROLE _MAIL _MANAGER : 'mailmanager' ,
2020-02-21 12:17:06 -08:00
ROLE _OWNER : 'owner' ,
2021-04-14 15:54:09 -07:00
compareRoles ,
2015-07-20 00:09:47 -07:00
} ;
2021-12-01 09:27:24 -08:00
const ORDERED _ROLES = [ exports . ROLE _USER , exports . ROLE _USER _MANAGER , exports . ROLE _MAIL _MANAGER , exports . ROLE _ADMIN , exports . ROLE _OWNER ] ;
2020-02-21 12:17:06 -08:00
2022-05-14 19:41:32 +02:00
// the avatar and backgroundImage fields are special and not added here to reduce response sizes
2024-02-26 12:32:14 +01:00
const USERS _FIELDS = [ 'id' , 'username' , 'email' , 'fallbackEmail' , 'password' , 'salt' , 'creationTime' , 'inviteToken' , 'resetToken' , 'displayName' , 'language' ,
2021-07-15 09:50:11 -07:00
'twoFactorAuthenticationEnabled' , 'twoFactorAuthenticationSecret' , 'active' , 'source' , 'role' , 'resetTokenCreationTime' , 'loginLocationsJson' ] . join ( ',' ) ;
2021-09-17 12:52:41 +02:00
const DEFAULT _GHOST _LIFETIME = 6 * 60 * 60 * 1000 ; // 6 hours
2021-09-17 11:48:56 +02:00
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' ) ,
2023-08-11 19:41:05 +05:30
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' ) ,
2018-06-11 12:38:15 -07:00
hat = require ( './hat.js' ) ,
2022-11-10 12:57:32 +01:00
mail = require ( './mail.js' ) ,
2016-02-08 15:15:42 -08:00
mailer = require ( './mailer.js' ) ,
2021-07-15 09:50:11 -07:00
mysql = require ( 'mysql' ) ,
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' ) ,
2024-02-26 12:32:14 +01:00
translation = require ( './translation.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 ) ;
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 ;
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-10-01 12:27:22 +02:00
function validateToken ( 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 ;
}
2024-02-26 12:32:14 +01:00
async function validateLanguage ( language ) {
assert . strictEqual ( typeof language , 'string' ) ;
if ( language === '' ) return null ; // reset to platform default
const languages = await translation . listLanguages ( ) ;
if ( ! languages . includes ( language ) ) return new BoxError ( BoxError . BAD _FIELD , 'Invalid language' ) ;
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-10-01 14:32:37 +02:00
const result = _ . pick ( user , 'id' , 'username' , 'email' , 'fallbackEmail' , 'displayName' , 'groupIds' , 'active' , 'source' , 'role' , 'createdAt' , 'twoFactorAuthenticationEnabled' ) ;
// invite status indicator
result . inviteAccepted = ! user . inviteToken ;
return result ;
2018-03-02 11:24:06 +01: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' ) ;
2021-10-28 10:29:02 +02:00
if ( 'fallbackEmail' in data ) assert . strictEqual ( typeof data . fallbackEmail , 'string' ) ;
2021-07-15 09:50:11 -07:00
2021-10-28 10:29:02 +02:00
let { username , password , displayName } = 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 ;
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
2021-10-26 22:50:02 +02:00
fallbackEmail = fallbackEmail . toLowerCase ( ) ;
if ( fallbackEmail ) {
2024-04-26 20:09:36 +02:00
error = validateEmail ( fallbackEmail ) ;
2021-10-26 22:50:02 +02:00
if ( error ) throw error ;
}
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 ;
let salt , derivedKey ;
2021-08-13 14:43:08 -07:00
[ error , salt ] = await safe ( randomBytesAsync ( CRYPTO _SALT _SIZE ) ) ;
2021-07-15 09:50:11 -07:00
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 ,
2021-10-26 22:50:02 +02:00
fallbackEmail : 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
2021-07-15 09:50:11 -07:00
displayName : displayName ,
source : source ,
role : role ,
2024-02-26 12:32:14 +01:00
avatar : constants . AVATAR _NONE ,
language : ''
2021-07-15 09:50:11 -07:00
} ;
2024-02-26 12:32:14 +01:00
const query = 'INSERT INTO users (id, username, password, email, fallbackEmail, salt, resetToken, inviteToken, displayName, source, role, avatar, language) 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 ] ;
2021-07-15 09:50:11 -07:00
[ 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 ;
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 ;
2015-07-20 00:09:47 -07:00
}
2021-09-17 12:52:41 +02:00
async function setGhost ( user , password , expiresAt ) {
assert . strictEqual ( typeof user , 'object' ) ;
2021-09-17 15:52:52 +02:00
assert . strictEqual ( typeof password , 'string' ) ;
assert . strictEqual ( typeof expiresAt , 'number' ) ;
2021-09-17 12:52:41 +02:00
2021-10-27 23:36:44 +02:00
if ( ! user . username ) throw new BoxError ( BoxError . BAD _STATE , 'user has no username yet' ) ;
2021-09-17 16:08:03 +02:00
expiresAt = expiresAt || ( Date . now ( ) + DEFAULT _GHOST _LIFETIME ) ;
2021-09-17 12:52:41 +02:00
debug ( ` setGhost: ${ user . username } expiresAt ${ expiresAt } ` ) ;
2023-08-03 11:34:33 +05:30
const ghostData = await settings . getJson ( settings . GHOSTS _CONFIG _KEY ) || { } ;
2021-09-17 12:52:41 +02:00
ghostData [ user . username ] = { password , expiresAt } ;
2023-08-03 11:34:33 +05:30
await settings . setJson ( settings . GHOSTS _CONFIG _KEY , ghostData ) ;
2021-09-17 12:52:41 +02:00
}
2016-07-12 10:07:55 -07:00
// returns true if ghost user was matched
2021-09-20 13:05:42 +02:00
async function verifyGhost ( username , password ) {
2016-07-12 10:07:55 -07:00
assert . strictEqual ( typeof username , 'string' ) ;
assert . strictEqual ( typeof password , 'string' ) ;
2023-08-03 11:34:33 +05:30
const ghostData = await settings . getJson ( settings . GHOSTS _CONFIG _KEY ) || { } ;
2016-07-12 10:07:55 -07:00
2021-09-17 11:48:56 +02:00
// 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 ] ;
2021-09-20 13:05:42 +02:00
2023-08-03 11:34:33 +05:30
await settings . setJson ( settings . GHOSTS _CONFIG _KEY , ghostData ) ;
2021-09-20 13:05:42 +02:00
2021-09-17 11:48:56 +02:00
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 ] ;
2021-09-20 13:05:42 +02:00
2023-08-03 11:34:33 +05:30
await settings . setJson ( settings . GHOSTS _CONFIG _KEY , ghostData ) ;
2021-09-17 11:48:56 +02:00
return true ;
}
2016-07-12 10:07:55 -07:00
}
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 ) ;
2024-04-26 20:09:36 +02:00
const 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-12-23 21:31:48 +01:00
// identifier is only used to check if password is valid for a specific app
2023-03-12 15:09:20 +01:00
async function verify ( userId , password , identifier , options ) {
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' ) ;
2023-03-12 15:09:20 +01:00
assert . strictEqual ( typeof options , 'object' ) ;
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
2021-09-20 13:05:42 +02:00
if ( user . username ) {
2021-09-20 10:34:26 -07:00
const valid = await verifyGhost ( user . username , password ) ;
2021-09-20 13:05:42 +02:00
if ( valid ) {
user . ghost = true ;
return user ;
}
2021-07-15 09:50:11 -07:00
}
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
2024-01-20 13:21:01 +01:00
let localTotpCheck = true ; // does 2fa need to be verified with local database 2fa creds
2021-07-15 09:50:11 -07:00
if ( user . source === 'ldap' ) {
2024-01-08 11:55:35 +01:00
await externalLdap . verifyPassword ( user . username , password , options ) ;
2024-01-20 13:21:01 +01:00
const externalLdapConfig = await externalLdap . getConfig ( ) ;
localTotpCheck = user . twoFactorAuthenticationEnabled && ! externalLdap . supports2FA ( externalLdapConfig ) ;
2021-07-15 09:50:11 -07:00
} 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' ) ;
2023-03-12 15:09:20 +01:00
if ( derivedKeyHex !== user . password ) throw new BoxError ( BoxError . INVALID _CREDENTIALS , 'Username and password does not match' ) ;
2024-01-20 13:21:01 +01:00
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' ) ;
2021-07-15 09:50:11 -07:00
}
return user ;
2016-04-05 16:27:04 +02:00
}
2023-03-12 15:09:20 +01:00
async function verifyWithUsername ( username , password , identifier , options ) {
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' ) ;
2023-03-12 15:09:20 +01:00
assert . strictEqual ( typeof options , 'object' ) ;
2015-07-20 00:09:47 -07:00
2021-07-15 09:50:11 -07:00
const user = await getByUsername ( username . toLowerCase ( ) ) ;
2023-03-12 15:09:20 +01:00
if ( user ) return await verify ( user . id , password , identifier , options ) ;
const [ error , newUserId ] = await safe ( externalLdap . maybeCreateUser ( username . toLowerCase ( ) ) ) ;
if ( error && error . reason === BoxError . BAD _STATE ) throw new BoxError ( BoxError . NOT _FOUND , 'User not found' ) ; // no external ldap or no auto create
if ( error ) {
2023-04-16 10:49:59 +02:00
debug ( ` verifyWithUsername: failed to auto create user ${ username } . %o ` , error ) ;
2023-03-12 15:09:20 +01:00
throw new BoxError ( BoxError . NOT _FOUND , 'User not found' ) ;
}
2015-07-20 00:09:47 -07:00
2023-03-12 15:09:20 +01:00
return await verify ( newUserId , password , identifier , options ) ;
2015-07-20 00:09:47 -07:00
}
2023-03-12 15:09:20 +01:00
async function verifyWithEmail ( email , password , identifier , options ) {
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' ) ;
2023-03-12 15:09:20 +01:00
assert . strictEqual ( typeof options , 'object' ) ;
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
2023-03-12 15:09:20 +01:00
return await verify ( user . id , password , identifier , options ) ;
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
2024-01-13 21:15:41 +01:00
if ( constants . DEMO && user . username === constants . DEMO _USERNAME ) throw new BoxError ( BoxError . BAD _STATE , '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
2022-02-24 20:04:46 -08:00
await eventlog . add ( eventlog . ACTION _USER _REMOVE , auditSource , { userId : user . id , user : removePrivateFields ( user ) } ) ;
2015-07-20 00:09:47 -07:00
}
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 ) {
2019-01-15 17:21:40 +01:00
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 ) ;
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' ) ;
2024-04-26 20:09:36 +02:00
const error = validateToken ( resetToken ) ;
2021-07-15 09:50:11 -07:00
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-10-01 12:27:22 +02:00
async function getByInviteToken ( inviteToken ) {
assert . strictEqual ( typeof inviteToken , 'string' ) ;
2024-04-26 20:09:36 +02:00
const error = validateToken ( inviteToken ) ;
2021-10-01 12:27:22 +02:00
if ( error ) throw error ;
const result = await database . query ( ` SELECT ${ USERS _FIELDS } FROM users WHERE inviteToken=? ` , [ inviteToken ] ) ;
if ( result . length === 0 ) return null ;
return postProcess ( result [ 0 ] ) ;
}
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
2024-01-13 21:15:41 +01:00
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
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 ) {
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' ) ;
2016-06-02 23:53:06 -07:00
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
}
2024-02-26 12:32:14 +01:00
if ( data . language ) {
error = await validateLanguage ( data . language ) ;
if ( error ) throw error ;
}
let args = [ ] ;
let 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' ) {
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
2023-05-25 11:27:23 +02:00
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 ) )
2016-09-21 15:34:58 -07:00
} ) ;
2015-07-20 00:09:47 -07:00
}
2024-01-20 10:41:24 +01:00
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 ) ;
}
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-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 ) ;
}
2023-08-11 19:41:05 +05:30
const { fqdn : dashboardFqdn } = await dashboard . getLocation ( ) ;
const resetLink = ` https:// ${ dashboardFqdn } /passwordreset.html?resetToken= ${ resetToken } ` ;
2021-09-16 14:34:56 +02:00
return resetLink ;
2015-07-20 00:09:47 -07:00
}
2022-11-10 12:57:32 +01:00
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' ) ;
2024-01-13 21:37:02 +01:00
if ( user . source === 'ldap' ) throw new BoxError ( BoxError . BAD _STATE , 'Cannot reset password of external auth user' ) ;
2022-11-10 12:57:32 +01:00
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' ) ;
2022-11-10 12:57:32 +01:00
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' ) ;
2021-10-28 11:18:31 +02:00
const error = validateEmail ( email ) ;
if ( error ) throw error ;
2022-11-10 12:57:32 +01:00
// 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' ) ;
2022-11-10 12:57:32 +01:00
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 ) {
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
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 ;
2021-04-30 13:21:50 +02:00
2021-07-29 11:26:23 +02:00
const response = await superagent . get ( 'https://geolocation.cloudron.io/json' ) . query ( { ip } ) . ok ( ( ) => true ) ;
2022-02-21 17:34:51 -08:00
if ( response . statusCode !== 200 ) return debug ( ` 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 } ;
2024-04-26 20:09:36 +02:00
const 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 ) ;
2021-08-19 12:32:23 -07:00
await 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
2024-01-13 21:15:41 +01:00
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' ) ;
2015-07-20 00:09:47 -07:00
2021-08-13 14:43:08 -07:00
let salt , derivedKey ;
[ error , salt ] = await safe ( randomBytesAsync ( CRYPTO _SALT _SIZE ) ) ;
2015-07-20 00:09:47 -07:00
2021-08-13 14:43:08 -07:00
[ error , derivedKey ] = await safe ( pbkdf2Async ( newPassword , salt , CRYPTO _ITERATIONS , CRYPTO _KEY _LENGTH , CRYPTO _DIGEST ) ) ;
2021-07-15 09:50:11 -07:00
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 = {
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 : ''
} ;
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-10-26 22:50:02 +02:00
return await add ( email , { username , password , fallbackEmail : '' , displayName , role : exports . ROLE _OWNER } , 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 ) {
2020-02-13 20:45:00 -08:00
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' ) ;
2021-10-27 21:25:43 +02:00
if ( ! user . inviteToken ) throw new BoxError ( BoxError . BAD _STATE , 'User already used invite link' ) ;
2019-10-29 11:03:28 -07:00
2023-08-03 08:11:42 +05:30
const directoryConfig = await getProfileConfig ( ) ;
2023-08-11 19:41:05 +05:30
const { fqdn : dashboardFqdn } = await dashboard . getLocation ( ) ;
let inviteLink = ` https:// ${ dashboardFqdn } /setupaccount.html?inviteToken= ${ user . inviteToken } &email= ${ encodeURIComponent ( user . email ) } ` ;
2021-09-16 14:56:10 +02:00
if ( user . username ) inviteLink += ` &username= ${ encodeURIComponent ( user . username ) } ` ;
if ( user . displayName ) inviteLink += ` &displayName= ${ encodeURIComponent ( user . displayName ) } ` ;
if ( directoryConfig . lockUserProfiles ) inviteLink += '&profileLocked=true' ;
2020-07-09 16:39:29 -07:00
2021-09-16 14:56:10 +02:00
return inviteLink ;
2020-07-09 16:39:29 -07:00
}
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' ) ;
2021-10-28 11:18:31 +02:00
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 ) {
2020-07-09 16:39:29 -07:00
assert . strictEqual ( typeof user , 'object' ) ;
assert . strictEqual ( typeof data , 'object' ) ;
assert ( auditSource && typeof auditSource === 'object' ) ;
2023-08-03 08:11:42 +05:30
const profileConfig = await getProfileConfig ( ) ;
2018-08-17 09:49:58 -07:00
2022-02-07 16:09:43 -08:00
const tmp = { inviteToken : '' } ;
2021-11-22 20:42:51 +01:00
2022-01-13 15:20:16 -08:00
if ( profileConfig . lockUserProfiles ) {
if ( ! user . username ) throw new BoxError ( BoxError . CONFLICT , 'Account cannot be setup without a username' ) ; // error out if admin has not provided a username
} else {
if ( data . username ) tmp . username = data . username ;
if ( data . displayName ) tmp . displayName = data . displayName ;
2021-11-22 20:42:51 +01:00
}
await update ( user , tmp , auditSource ) ;
2020-07-09 16:39:29 -07:00
2021-10-01 12:27:22 +02:00
await setPassword ( user , data . password , auditSource ) ;
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
}
2024-01-20 11:35:27 +01: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
2024-01-20 11:35:27 +01: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
2024-01-13 21:15:41 +01:00
if ( constants . DEMO && user . username === constants . DEMO _USERNAME ) throw new BoxError ( BoxError . BAD _STATE , '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
2023-08-11 19:41:05 +05:30
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
}
2024-01-20 11:35:27 +01: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
2024-01-20 11:35:27 +01: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 ) ;
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
}
2024-01-20 11:35:27 +01: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
2024-01-20 11:35:27 +01: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
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' ) ;
2024-04-26 20:09:36 +02:00
const roleInt1 = ORDERED _ROLES . indexOf ( role1 ) ;
const roleInt2 = ORDERED _ROLES . indexOf ( role2 ) ;
2020-02-21 12:17:06 -08:00
return roleInt1 - roleInt2 ;
}
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
}
2022-05-14 19:41:32 +02:00
async function getBackgroundImage ( id ) {
assert . strictEqual ( typeof id , 'string' ) ;
const result = await database . query ( 'SELECT backgroundImage FROM users WHERE id = ?' , [ id ] ) ;
if ( result . length === 0 ) throw new BoxError ( BoxError . NOT _FOUND , 'User not found' ) ;
return result [ 0 ] . backgroundImage ;
}
async function setBackgroundImage ( id , backgroundImage ) {
assert . strictEqual ( typeof id , 'string' ) ;
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 , id ] ) ;
if ( result . length === 0 ) throw new BoxError ( BoxError . NOT _FOUND , 'User not found' ) ;
}
2023-08-03 08:11:42 +05:30
async function getProfileConfig ( ) {
2023-08-03 11:34:33 +05:30
const value = await settings . getJson ( settings . PROFILE _CONFIG _KEY ) ;
return value || { lockUserProfiles : false , mandatory2FA : false } ;
2023-08-03 08:11:42 +05:30
}
async function setProfileConfig ( profileConfig ) {
assert . strictEqual ( typeof profileConfig , 'object' ) ;
2024-01-13 21:15:41 +01:00
if ( constants . DEMO ) throw new BoxError ( BoxError . BAD _STATE , 'Not allowed in demo mode' ) ;
2023-08-03 08:11:42 +05:30
const oldConfig = await getProfileConfig ( ) ;
2023-08-03 11:34:33 +05:30
await settings . setJson ( settings . PROFILE _CONFIG _KEY , profileConfig ) ;
2023-08-03 08:11:42 +05:30
if ( profileConfig . mandatory2FA && ! oldConfig . mandatory2FA ) {
debug ( 'setProfileConfig: logging out non-2FA users to enforce 2FA' ) ;
const allUsers = await list ( ) ;
for ( const user of allUsers ) {
if ( ! user . twoFactorAuthenticationEnabled ) await tokens . delByUserIdAndType ( user . id , tokens . ID _WEBADMIN ) ;
}
}
}
2024-01-13 11:27:08 +01:00
async function resetSource ( ) {
await database . query ( 'UPDATE users SET source = ?' , [ '' ] ) ;
}
2024-02-06 16:43:05 +01:00
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 } ;
}