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
2025-07-11 17:59:00 +02:00
verifyWithId ,
2021-07-15 09:50:11 -07:00
verifyWithUsername ,
verifyWithEmail ,
2021-04-14 15:54:09 -07:00
setPassword ,
2021-09-17 12:52:41 +02:00
setGhost ,
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 ,
2024-12-11 18:24:20 +01:00
setNotificationConfig ,
2024-12-04 09:48:25 +01:00
resetSources ,
2024-01-13 11:27:08 +01:00
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-06-25 22:11:17 -07:00
const appPasswords = require ( './apppasswords.js' ) ,
2025-08-14 11:17:38 +05:30
assert = require ( 'node:assert' ) ,
2019-10-22 16:34:17 -07:00
BoxError = require ( './boxerror.js' ) ,
2025-08-14 11:17:38 +05:30
crypto = require ( 'node: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' ) ,
2025-06-19 12:31:54 +02:00
mysql = require ( 'mysql2' ) ,
2024-12-11 20:44:46 +01:00
notifications = require ( './notifications' ) ,
2025-06-11 22:53:29 +02:00
oidcClients = require ( './oidcclients.js' ) ,
2018-04-25 19:08:15 +02:00
qrcode = require ( 'qrcode' ) ,
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-07-05 12:45:23 +02:00
translations = require ( './translations.js' ) ,
2021-05-04 09:11:16 +02:00
uaParser = require ( 'ua-parser-js' ) ,
2024-06-12 10:27:59 +02:00
userDirectory = require ( './user-directory.js' ) ,
2025-07-10 10:55:52 +02:00
superagent = require ( '@cloudron/superagent' ) ,
2025-08-14 11:17:38 +05:30
util = require ( 'node:util' ) ,
2025-03-07 12:07:33 +01:00
validator = require ( './validator.js' ) ,
2025-02-13 14:03:25 +01:00
_ = require ( './underscore.js' ) ;
2015-07-20 00:09:47 -07:00
2025-10-08 20:11:55 +02:00
// the avatar and backgroundImage fields are special and not added here to reduce response sizes
const USERS _FIELDS = [ 'id' , 'username' , 'email' , 'fallbackEmail' , 'password' , 'salt' , 'creationTime' , 'inviteToken' , 'resetToken' , 'displayName' , 'language' ,
'twoFactorAuthenticationEnabled' , 'twoFactorAuthenticationSecret' , 'active' , 'source' , 'role' , 'resetTokenCreationTime' , 'loginLocationsJson' , 'notificationConfigJson' ] . join ( ',' ) ;
const DEFAULT _GHOST _LIFETIME = 6 * 60 * 60 * 1000 ; // 6 hours
2021-06-25 22:11:17 -07:00
const CRYPTO _SALT _SIZE = 64 ; // 512-bit salt
const CRYPTO _ITERATIONS = 10000 ; // iterations
const CRYPTO _KEY _LENGTH = 512 ; // bits
const CRYPTO _DIGEST = 'sha1' ; // used to be the default in node 4.1.1 cannot change since it will affect existing db records
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 ;
2024-12-11 18:24:20 +01:00
result . notificationConfig = safe . JSON . parse ( result . notificationConfigJson ) || [ ] ;
delete result . notificationConfigJson ;
2021-07-15 09:50:11 -07:00
return result ;
}
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
2025-01-25 16:03:10 +01:00
if ( username . endsWith ( '.app' ) ) 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
2024-07-05 12:45:23 +02:00
const languages = await translations . listLanguages ( ) ;
2024-02-26 12:32:14 +01:00
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 ) {
2025-02-13 14:03:25 +01:00
const result = _ . pick ( user , [
'id' , 'username' , 'email' , 'fallbackEmail' , 'displayName' , 'groupIds' , 'active' , 'source' , 'role' , 'createdAt' ,
'twoFactorAuthenticationEnabled' , 'notificationConfig' ] ) ;
2021-10-01 14:32:37 +02:00
// invite status indicator
result . inviteAccepted = ! user . inviteToken ;
return result ;
2018-03-02 11:24:06 +01:00
}
2025-10-08 20:11:55 +02:00
function validateRole ( role ) {
assert . strictEqual ( typeof role , 'string' ) ;
const ORDERED _ROLES = [ exports . ROLE _USER , exports . ROLE _USER _MANAGER , exports . ROLE _MAIL _MANAGER , exports . ROLE _ADMIN , exports . ROLE _OWNER ] ;
if ( ORDERED _ROLES . includes ( role ) ) return null ;
return new BoxError ( BoxError . BAD _FIELD , ` Invalid role ' ${ role } ' ` ) ;
}
function compareRoles ( role1 , role2 ) {
assert . strictEqual ( typeof role1 , 'string' ) ;
assert . strictEqual ( typeof role2 , 'string' ) ;
const ORDERED _ROLES = [ exports . ROLE _USER , exports . ROLE _USER _MANAGER , exports . ROLE _MAIL _MANAGER , exports . ROLE _ADMIN , exports . ROLE _OWNER ] ;
const roleInt1 = ORDERED _ROLES . indexOf ( role1 ) ;
const roleInt2 = ORDERED _ROLES . indexOf ( role2 ) ;
return roleInt1 - roleInt2 ;
}
2021-07-15 09:50:11 -07:00
async function add ( email , data , auditSource ) {
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
2024-05-25 13:42:29 +02:00
const { displayName } = data ;
let { username , password } = data ;
2021-10-28 10:29:02 +02:00
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 ;
2024-12-11 20:44:46 +01:00
const notificationConfig = 'notificationConfig' in data ? data . notificationConfig : null ;
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 ;
2024-05-25 13:42:29 +02:00
const [ randomBytesError , salt ] = await safe ( randomBytesAsync ( CRYPTO _SALT _SIZE ) ) ;
if ( randomBytesError ) throw new BoxError ( BoxError . CRYPTO _ERROR , randomBytesError ) ;
2021-07-15 09:50:11 -07:00
2024-05-25 13:42:29 +02:00
const [ pbkdf2Error , derivedKey ] = await safe ( pbkdf2Async ( password , salt , CRYPTO _ITERATIONS , CRYPTO _KEY _LENGTH , CRYPTO _DIGEST ) ) ;
if ( pbkdf2Error ) throw new BoxError ( BoxError . CRYPTO _ERROR , pbkdf2Error ) ;
2021-07-15 09:50:11 -07:00
const user = {
2025-07-28 12:53:27 +02:00
id : 'uid-' + crypto . randomUUID ( ) ,
2024-05-25 13:42:29 +02:00
username ,
email ,
fallbackEmail ,
2021-07-15 09:50:11 -07:00
password : Buffer . from ( derivedKey , 'binary' ) . toString ( 'hex' ) ,
salt : salt . toString ( 'hex' ) ,
resetToken : '' ,
2021-10-01 14:45:26 +02:00
inviteToken : hat ( 256 ) , // new users start out with invite tokens
2024-05-25 13:42:29 +02:00
displayName ,
source ,
role ,
2025-06-08 12:42:13 +02:00
avatar : null ,
2024-12-11 20:44:46 +01:00
language : '' ,
notificationConfigJson : notificationConfig ? JSON . stringify ( notificationConfig ) : null
2021-07-15 09:50:11 -07:00
} ;
2024-12-11 20:44:46 +01:00
const query = 'INSERT INTO users (id, username, password, email, fallbackEmail, salt, resetToken, inviteToken, displayName, source, role, avatar, language, notificationConfigJson) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)' ;
const args = [ user . id , user . username , user . password , user . email , user . fallbackEmail , user . salt , user . resetToken , user . inviteToken , user . displayName , user . source , user . role , user . avatar , user . language , user . notificationConfigJson ] ;
2021-07-15 09:50:11 -07:00
[ error ] = await safe ( database . query ( query , args ) ) ;
2025-09-29 11:55:15 +02:00
if ( error && error . sqlCode === 'ER_DUP_ENTRY' && error . sqlMessage . indexOf ( 'users_email' ) !== - 1 ) throw new BoxError ( BoxError . ALREADY _EXISTS , 'email already exists' ) ;
if ( error && error . sqlCode === 'ER_DUP_ENTRY' && error . sqlMessage . indexOf ( 'users_username' ) !== - 1 ) throw new BoxError ( BoxError . ALREADY _EXISTS , 'username already exists' ) ;
if ( error && error . sqlCode === 'ER_DUP_ENTRY' && error . sqlMessage . indexOf ( 'PRIMARY' ) !== - 1 ) throw new BoxError ( BoxError . ALREADY _EXISTS , 'id already exists' ) ;
2021-07-15 09:50:11 -07:00
if ( error ) throw error ;
// when this is used to create the owner, then we have to patch the auditSource to contain himself
if ( ! auditSource . userId ) auditSource . userId = user . id ;
if ( ! auditSource . username ) auditSource . username = user . username ;
2022-02-24 20:04:46 -08:00
await eventlog . add ( eventlog . ACTION _USER _ADD , auditSource , { userId : user . id , email : user . email , user : removePrivateFields ( user ) } ) ;
2021-07-15 09:50:11 -07:00
2021-07-19 12:43:30 -07:00
return user . id ;
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
2025-10-20 16:37:14 +02:00
const arSearch = ` JSON_SEARCH(accessRestrictionJson, 'one', ?, NULL, ' $ .users') ` ;
const opSearch = ` JSON_SEARCH(operatorsJson, 'one', ?, NULL, ' $ .users') ` ;
const queries = [
{ query : ` UPDATE apps SET accessRestrictionJson=JSON_REMOVE(accessRestrictionJson, REPLACE( ${ arSearch } , '"', '')) WHERE ${ arSearch } IS NOT NULL ` , args : [ user . id , user . id ] } ,
{ query : ` UPDATE apps SET operatorsJson=JSON_REMOVE(operatorsJson, REPLACE( ${ opSearch } , '"', '')) WHERE ${ opSearch } IS NOT NULL ` , args : [ user . id , user . id ] } ,
{ query : 'DELETE FROM groupMembers WHERE userId = ?' , args : [ user . id ] } ,
{ query : 'DELETE FROM tokens WHERE identifier = ?' , args : [ user . id ] } ,
{ query : 'DELETE FROM appPasswords WHERE userId = ?' , args : [ user . id ] } ,
{ query : 'DELETE FROM users WHERE id = ?' , args : [ user . id ] } , // keep this the last query as we check affectedRows below
] ;
2016-05-01 20:09:31 -07:00
2021-06-26 09:57:07 -07:00
const [ error , result ] = await safe ( database . transaction ( queries ) ) ;
2025-09-29 11:55:15 +02:00
if ( error && error . sqlCode === 'ER_NO_REFERENCED_ROW_2' ) throw new BoxError ( BoxError . NOT _FOUND , error ) ;
2021-06-26 09:57:07 -07:00
if ( error ) throw error ;
2025-10-20 16:37:14 +02:00
if ( result [ queries . length - 1 ] . affectedRows !== 1 ) throw new BoxError ( BoxError . NOT _FOUND , 'User not found' ) ;
2021-06-03 11:42:32 -07:00
2022-02-24 20:04:46 -08:00
await eventlog . add ( eventlog . ACTION _USER _REMOVE , auditSource , { userId : user . id , user : removePrivateFields ( user ) } ) ;
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
}
2025-08-07 12:28:55 +02:00
async function internalGet ( fieldName , fieldValue ) {
assert . strictEqual ( typeof fieldName , 'string' ) ;
assert . strictEqual ( typeof fieldValue , 'string' ) ;
2015-07-20 00:09:47 -07:00
2021-07-15 09:50:11 -07:00
const results = await database . query ( ` SELECT ${ USERS _FIELDS } ,GROUP_CONCAT(groupMembers.groupId) AS groupIds ` +
2025-08-07 12:28:55 +02:00
' FROM users LEFT OUTER JOIN groupMembers ON users.id = groupMembers.userId ' +
` GROUP BY users.id HAVING users. ${ fieldName } = ? ` , [ fieldValue ] ) ;
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 ] ) ;
}
2025-08-07 12:28:55 +02:00
async function get ( userId ) {
assert . strictEqual ( typeof userId , 'string' ) ;
return await internalGet ( 'id' , userId ) ;
2015-07-20 00:09:47 -07:00
}
2025-08-07 12:28:55 +02:00
async function getByEmail ( email ) {
assert . strictEqual ( typeof email , 'string' ) ;
return await internalGet ( 'email' , email ) ;
2021-07-15 09:50:11 -07:00
}
async function getByResetToken ( resetToken ) {
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
2025-08-07 12:28:55 +02:00
return await internalGet ( 'resetToken' , resetToken ) ;
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 ;
2025-08-07 12:28:55 +02:00
return await internalGet ( 'inviteToken' , inviteToken ) ;
2021-10-01 12:27:22 +02: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' ) ;
2025-08-07 12:28:55 +02:00
return await internalGet ( 'username' , username ) ;
2019-03-18 21:15:50 -07:00
}
2021-07-15 09:50:11 -07:00
async function update ( user , data , auditSource ) {
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
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 ( ) ;
2024-05-25 13:42:29 +02:00
const error = validateUsername ( data . username ) ;
2021-07-15 09:50:11 -07:00
if ( error ) throw error ;
2016-06-02 23:53:06 -07:00
}
if ( data . email ) {
data . email = data . email . toLowerCase ( ) ;
2024-05-25 13:42:29 +02:00
const error = validateEmail ( data . email ) ;
2021-07-15 09:50:11 -07:00
if ( error ) throw error ;
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 ( ) ;
2024-05-25 13:42:29 +02:00
const error = validateEmail ( data . fallbackEmail ) ;
2021-07-15 09:50:11 -07:00
if ( error ) throw error ;
2018-01-21 14:25:39 +01:00
}
2020-02-21 12:17:06 -08:00
if ( data . role ) {
2024-05-25 13:42:29 +02:00
const error = validateRole ( data . role ) ;
2021-07-15 09:50:11 -07:00
if ( error ) throw error ;
2020-02-13 22:06:54 -08:00
}
2024-02-26 12:32:14 +01:00
if ( data . language ) {
2024-05-25 13:42:29 +02:00
const error = await validateLanguage ( data . language ) ;
2024-02-26 12:32:14 +01:00
if ( error ) throw error ;
}
2024-05-25 13:42:29 +02:00
const args = [ ] , fields = [ ] ;
2021-07-15 09:50:11 -07:00
for ( const k in data ) {
if ( k === 'twoFactorAuthenticationEnabled' || k === 'active' ) {
fields . push ( k + ' = ?' ) ;
args . push ( data [ k ] ? 1 : 0 ) ;
2024-12-11 20:44:46 +01:00
} else if ( k === 'loginLocations' || k === 'notificationConfig' ) {
fields . push ( ` ${ k } Json = ? ` ) ;
2021-07-15 09:50:11 -07:00
args . push ( JSON . stringify ( data [ k ] ) ) ;
} else {
fields . push ( k + ' = ?' ) ;
args . push ( data [ k ] ) ;
}
}
2025-02-13 17:28:02 +01:00
if ( args . length == 0 ) return ; // nothing to do
2021-07-15 09:50:11 -07:00
args . push ( user . id ) ;
2016-05-01 20:09:31 -07:00
2024-05-25 13:42:29 +02:00
const [ error , result ] = await safe ( database . query ( 'UPDATE users SET ' + fields . join ( ', ' ) + ' WHERE id = ?' , args ) ) ;
2025-09-29 11:55:15 +02:00
if ( error && error . sqlCode === 'ER_DUP_ENTRY' && error . sqlMessage . indexOf ( 'users_email' ) !== - 1 ) throw new BoxError ( BoxError . ALREADY _EXISTS , 'email already exists' ) ;
if ( error && error . sqlCode === 'ER_DUP_ENTRY' && error . sqlMessage . indexOf ( 'users_username' ) !== - 1 ) throw new BoxError ( BoxError . ALREADY _EXISTS , 'username already exists' ) ;
2021-07-15 09:50:11 -07:00
if ( error ) throw new BoxError ( BoxError . DATABASE _ERROR , error ) ;
if ( result . affectedRows !== 1 ) throw new BoxError ( BoxError . NOT _FOUND , 'User not found' ) ;
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 ) ;
}
2025-08-07 12:28:55 +02:00
async function listByRole ( role ) {
assert . strictEqual ( typeof role , 'string' ) ;
// the mailer code relies on the first object being the 'owner' (thus the ORDER)
const results = await database . query ( ` SELECT ${ USERS _FIELDS } ,GROUP_CONCAT(groupMembers.groupId) AS groupIds ` +
' FROM users LEFT OUTER JOIN groupMembers ON users.id = groupMembers.userId ' +
' GROUP BY users.id HAVING role = ? ORDER BY users.creationTime' , [ role ] ) ;
results . forEach ( function ( result ) {
result . groupIds = result . groupIds ? result . groupIds . split ( ',' ) : [ ] ;
} ) ;
results . forEach ( postProcess ) ;
return results ;
}
2021-07-15 09:50:11 -07:00
async function getOwner ( ) {
2025-08-07 12:28:55 +02:00
const owners = await listByRole ( exports . ROLE _OWNER ) ;
2021-07-15 09:50:11 -07:00
if ( owners . length === 0 ) return null ;
return owners [ 0 ] ;
2016-02-09 15:47:02 -08:00
}
2021-07-15 09:50:11 -07:00
async function getAdmins ( ) {
2025-08-07 12:28:55 +02:00
const owners = await listByRole ( exports . ROLE _OWNER ) ;
const admins = await listByRole ( 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 ( ) {
2025-08-07 12:28:55 +02:00
return await listByRole ( exports . ROLE _OWNER ) ;
2021-04-19 20:52:10 -07:00
}
2025-10-08 20:11:55 +02:00
async function setGhost ( user , password , expiresAt ) {
assert . strictEqual ( typeof user , 'object' ) ;
assert . strictEqual ( typeof password , 'string' ) ;
assert . strictEqual ( typeof expiresAt , 'number' ) ;
if ( ! user . username ) throw new BoxError ( BoxError . BAD _STATE , 'user has no username yet' ) ;
expiresAt = expiresAt || ( Date . now ( ) + DEFAULT _GHOST _LIFETIME ) ;
const ghostData = await settings . getJson ( settings . GHOSTS _CONFIG _KEY ) || { } ;
ghostData [ user . username ] = { password , expiresAt } ;
await settings . setJson ( settings . GHOSTS _CONFIG _KEY , ghostData ) ;
}
// returns true if ghost user was matched
async function verifyGhost ( username , password ) {
assert . strictEqual ( typeof username , 'string' ) ;
assert . strictEqual ( typeof password , 'string' ) ;
const ghostData = await settings . getJson ( settings . GHOSTS _CONFIG _KEY ) || { } ;
// either the username is an object with { password, expiresAt } or a string with the password which will expire on first match
if ( username in ghostData ) {
if ( typeof ghostData [ username ] === 'object' ) {
if ( ghostData [ username ] . expiresAt < Date . now ( ) ) {
debug ( 'verifyGhost: password expired' ) ;
delete ghostData [ username ] ;
await settings . setJson ( settings . GHOSTS _CONFIG _KEY , ghostData ) ;
return false ;
} else if ( ghostData [ username ] . password === password ) {
debug ( 'verifyGhost: matched ghost user' ) ;
return true ;
} else {
return false ;
}
} else if ( ghostData [ username ] === password ) {
debug ( 'verifyGhost: matched ghost user' ) ;
delete ghostData [ username ] ;
await settings . setJson ( settings . GHOSTS _CONFIG _KEY , ghostData ) ;
return true ;
}
}
return false ;
}
async function verifyAppPassword ( userId , password , identifier ) {
assert . strictEqual ( typeof userId , 'string' ) ;
assert . strictEqual ( typeof password , 'string' ) ;
assert . strictEqual ( typeof identifier , 'string' ) ;
const results = await appPasswords . list ( userId ) ;
const hashedPasswords = results . filter ( r => r . identifier === identifier ) . map ( r => r . hashedPassword ) ;
const hash = crypto . createHash ( 'sha256' ) . update ( password ) . digest ( 'base64' ) ;
if ( hashedPasswords . includes ( hash ) ) return ;
throw new BoxError ( BoxError . INVALID _CREDENTIALS , 'Password is not valid' ) ;
}
// identifier is only used to check if password is valid for a specific app
async function verify ( user , password , identifier , options ) {
assert . strictEqual ( typeof user , 'object' ) ;
assert . strictEqual ( typeof password , 'string' ) ;
assert . strictEqual ( typeof identifier , 'string' ) ;
assert . strictEqual ( typeof options , 'object' ) ;
if ( ! user . active ) {
debug ( ` verify: ${ user . username } is not active ` ) ;
throw new BoxError ( BoxError . NOT _FOUND , 'User not active' ) ;
}
// for just invited users the username may be still null
if ( user . username ) {
const valid = await verifyGhost ( user . username , password ) ;
if ( valid ) {
debug ( ` verify: ${ user . username } authenticated via impersonation ` ) ;
user . ghost = true ;
return user ;
}
}
const [ error ] = await safe ( verifyAppPassword ( user . id , password , identifier ) ) ;
if ( ! error ) { // matched app password
debug ( ` verify: ${ user . username || user . id } matched app password ` ) ;
user . appPassword = true ;
return user ;
}
let localTotpCheck = true ; // does 2fa need to be verified with local database 2fa creds
if ( user . source === 'ldap' ) {
await externalLdap . verifyPassword ( user . username , password , options ) ;
const externalLdapConfig = await externalLdap . getConfig ( ) ;
localTotpCheck = user . twoFactorAuthenticationEnabled && ! externalLdap . supports2FA ( externalLdapConfig ) ;
} else {
const saltBinary = Buffer . from ( user . salt , 'hex' ) ;
const [ error , derivedKey ] = await safe ( pbkdf2Async ( password , saltBinary , CRYPTO _ITERATIONS , CRYPTO _KEY _LENGTH , CRYPTO _DIGEST ) ) ;
if ( error ) throw new BoxError ( BoxError . CRYPTO _ERROR , error ) ;
const derivedKeyHex = Buffer . from ( derivedKey , 'binary' ) . toString ( 'hex' ) ;
if ( derivedKeyHex !== user . password ) {
debug ( ` verify: ${ user . username || user . id } provided incorrect password ` ) ;
throw new BoxError ( BoxError . INVALID _CREDENTIALS , 'Wrong password' ) ;
}
localTotpCheck = user . twoFactorAuthenticationEnabled ;
}
if ( localTotpCheck && ! options . skipTotpCheck ) {
if ( ! options . totpToken ) throw new BoxError ( BoxError . INVALID _CREDENTIALS , 'A totpToken must be provided' ) ;
const verified = speakeasy . totp . verify ( { secret : user . twoFactorAuthenticationSecret , encoding : 'base32' , token : options . totpToken , window : 2 } ) ;
if ( ! verified ) throw new BoxError ( BoxError . INVALID _CREDENTIALS , 'Invalid totpToken' ) ;
}
return user ;
}
async function verifyWithId ( id , password , identifier , options ) {
assert . strictEqual ( typeof id , 'string' ) ;
assert . strictEqual ( typeof password , 'string' ) ;
assert . strictEqual ( typeof identifier , 'string' ) ;
assert . strictEqual ( typeof options , 'object' ) ;
const user = await get ( id ) ;
if ( ! user ) {
debug ( ` verifyWithId: ${ id } not found ` ) ;
throw new BoxError ( BoxError . NOT _FOUND , 'User not found' ) ;
}
return await verify ( user , password , identifier , options ) ;
}
async function verifyWithUsername ( username , password , identifier , options ) {
assert . strictEqual ( typeof username , 'string' ) ;
assert . strictEqual ( typeof password , 'string' ) ;
assert . strictEqual ( typeof identifier , 'string' ) ;
assert . strictEqual ( typeof options , 'object' ) ;
const user = await getByUsername ( username . toLowerCase ( ) ) ;
if ( user ) return await verify ( user , password , identifier , options ) ;
const [ error , newUserId ] = await safe ( externalLdap . maybeCreateUser ( username . toLowerCase ( ) ) ) ;
if ( error && error . reason === BoxError . BAD _STATE ) {
debug ( ` verifyWithUsername: ${ username } not found ` ) ;
throw new BoxError ( BoxError . NOT _FOUND , 'User not found' ) ; // no external ldap or no auto create
}
if ( error ) {
debug ( ` verifyWithUsername: failed to auto create user ${ username } . %o ` , error ) ;
throw new BoxError ( BoxError . NOT _FOUND , 'User not found' ) ;
}
return await verifyWithId ( newUserId , password , identifier , options ) ;
}
async function verifyWithEmail ( email , password , identifier , options ) {
assert . strictEqual ( typeof email , 'string' ) ;
assert . strictEqual ( typeof password , 'string' ) ;
assert . strictEqual ( typeof identifier , 'string' ) ;
assert . strictEqual ( typeof options , 'object' ) ;
const user = await getByEmail ( email . toLowerCase ( ) ) ;
if ( ! user ) {
debug ( ` verifyWithEmail: ${ email } no such user ` ) ;
throw new BoxError ( BoxError . NOT _FOUND , 'User not found' ) ;
}
return await verify ( user , password , identifier , options ) ;
}
2021-10-27 18:36:28 +02:00
async function getPasswordResetLink ( user , auditSource ) {
assert . strictEqual ( typeof user , 'object' ) ;
assert . strictEqual ( typeof auditSource , 'object' ) ;
let resetToken = user . resetToken ;
let resetTokenCreationTime = user . resetTokenCreationTime || 0 ;
if ( ! resetToken || ( Date . now ( ) - resetTokenCreationTime > 7 * 24 * 60 * 60 * 1000 ) ) {
resetToken = hat ( 256 ) ;
resetTokenCreationTime = new Date ( ) ;
await update ( user , { resetToken , resetTokenCreationTime } , auditSource ) ;
}
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
2025-06-11 23:38:32 +02:00
debug ( ` notifyLoginLocation: ${ user . id } ${ ip } ${ userAgent } ghost: ${ ! ! user . ghost } ` ) ;
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 ;
2024-06-13 16:47:08 +02:00
if ( user . ghost || user . source ) return ; // for external users, rely on the external source to send login notification to avoid dup login emails
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 ) ;
2025-02-14 17:26:54 +01:00
if ( response . status !== 200 ) return debug ( ` Failed to get geoip info. status: ${ response . status } ` ) ;
2021-04-30 09:44:25 -07:00
2021-07-15 09:50:11 -07:00
const country = safe . query ( response . body , 'country.names.en' , '' ) ;
const city = safe . query ( response . body , 'city.names.en' , '' ) ;
2021-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 ) ;
2025-08-06 10:18:05 +02:00
await safe ( mailer . sendNewLoginLocation ( user , newLoginLocation ) , { debug } ) ;
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
2024-05-25 13:42:29 +02:00
const error = validatePassword ( newPassword ) ;
2021-07-15 09:50:11 -07:00
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
2024-05-25 13:42:29 +02:00
const [ randomBytesError , salt ] = await safe ( randomBytesAsync ( CRYPTO _SALT _SIZE ) ) ;
if ( randomBytesError ) throw new BoxError ( BoxError . CRYPTO _ERROR , randomBytesError ) ;
2015-07-20 00:09:47 -07:00
2024-05-25 13:42:29 +02:00
const [ pbkdf2Error , derivedKey ] = await safe ( pbkdf2Async ( newPassword , salt , CRYPTO _ITERATIONS , CRYPTO _KEY _LENGTH , CRYPTO _DIGEST ) ) ;
if ( pbkdf2Error ) throw new BoxError ( BoxError . CRYPTO _ERROR , pbkdf2Error ) ;
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
2025-01-24 18:51:04 +01:00
const notificationConfig = [ notifications . TYPE _BACKUP _FAILED , notifications . TYPE _CERTIFICATE _RENEWAL _FAILED , notifications . TYPE _MANUAL _APP _UPDATE _NEEDED , notifications . TYPE _APP _DOWN , notifications . TYPE _CLOUDRON _UPDATE _FAILED ] ;
2024-12-11 20:44:46 +01:00
return await add ( email , { username , password , fallbackEmail : '' , displayName , role : exports . ROLE _OWNER , notificationConfig } , auditSource ) ;
2016-01-13 12:28:38 -08:00
}
2016-01-18 15:16:18 +01:00
2021-10-27 19:58:06 +02:00
async function getInviteLink ( user , auditSource ) {
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
2024-06-12 10:27:59 +02:00
const directoryConfig = await userDirectory . 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' ) ;
2025-08-14 16:02:54 +05:30
const profileConfig = await userDirectory . getProfileConfig ( ) ;
// error out if admin has not provided a username
if ( profileConfig . lockUserProfiles ) {
if ( ! user . username ) throw new BoxError ( BoxError . CONFLICT , 'Account cannot be setup without a username' ) ;
if ( data . username ) throw new BoxError ( BoxError . CONFLICT , 'Username cannot be changed because profiles are locked' ) ;
}
2022-02-07 16:09:43 -08:00
const tmp = { inviteToken : '' } ;
2021-11-22 20:42:51 +01:00
2025-08-08 10:07:14 +02:00
if ( data . username ) {
const error = validateUsername ( data . username ) ;
if ( error ) throw error ;
tmp . username = data . username ;
2021-11-22 20:42:51 +01:00
}
2025-08-08 10:07:14 +02:00
if ( data . displayName ) {
const error = validateDisplayName ( data . displayName ) ;
if ( error ) throw error ;
2020-07-09 16:39:29 -07:00
2025-08-08 10:07:14 +02:00
tmp . displayName = data . displayName ;
}
const error = validatePassword ( data . password ) ;
if ( error ) throw error ;
await update ( user , tmp , auditSource ) ;
2021-10-01 12:27:22 +02:00
await setPassword ( user , data . password , auditSource ) ;
2020-07-09 16:39:29 -07:00
2025-06-11 22:53:29 +02:00
const token = { clientId : oidcClients . ID _WEBADMIN , identifier : user . id , expires : Date . now ( ) + constants . DEFAULT _TOKEN _EXPIRATION _MSECS , allowedIpRanges : '' } ;
2021-07-15 09:50:11 -07:00
const result = await tokens . add ( token ) ;
return result . accessToken ;
2018-08-17 09:49:58 -07:00
}
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
2024-10-30 16:21:21 +01:00
if ( user . twoFactorAuthenticationEnabled ) throw new BoxError ( BoxError . ALREADY _EXISTS , '2FA is already enabled' ) ;
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 } ) ;
2024-10-30 16:21:21 +01:00
if ( ! verified ) throw new BoxError ( BoxError . INVALID _CREDENTIALS , 'Invalid 2FA code' ) ;
2018-04-25 19:08:15 +02:00
2024-10-30 16:21:21 +01:00
if ( user . twoFactorAuthenticationEnabled ) throw new BoxError ( BoxError . ALREADY _EXISTS , '2FA already enabled' ) ;
2018-04-25 19:08:15 +02:00
2021-07-15 09:50:11 -07:00
await update ( user , { twoFactorAuthenticationEnabled : true } , auditSource ) ;
2018-04-25 19:08:15 +02:00
}
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
2025-06-08 12:42:13 +02:00
async function getAvatar ( user ) {
assert . strictEqual ( typeof user , 'object' ) ;
2020-07-09 22:33:36 -07:00
2025-06-08 12:42:13 +02:00
const result = await database . query ( 'SELECT avatar FROM users WHERE id = ?' , [ user . id ] ) ;
2021-07-07 14:31:39 +02:00
if ( result . length === 0 ) throw new BoxError ( BoxError . NOT _FOUND , 'User not found' ) ;
2021-06-25 22:11:17 -07:00
return result [ 0 ] . avatar ;
2020-07-09 22:33:36 -07:00
}
2025-06-08 12:42:13 +02:00
async function setAvatar ( user , avatar ) {
assert . strictEqual ( typeof user , 'object' ) ;
2025-07-15 10:06:26 +02:00
assert ( Buffer . isBuffer ( avatar ) || avatar === null ) ;
2020-07-09 22:33:36 -07:00
2025-06-08 12:42:13 +02:00
const result = await database . query ( 'UPDATE users SET avatar=? WHERE id = ?' , [ avatar , user . id ] ) ;
2021-06-25 22:11:17 -07:00
if ( result . length === 0 ) throw new BoxError ( BoxError . NOT _FOUND , 'User not found' ) ;
2020-07-09 22:33:36 -07:00
}
2022-05-14 19:41:32 +02:00
2025-06-08 12:42:13 +02:00
async function getBackgroundImage ( user ) {
assert . strictEqual ( typeof user , 'object' ) ;
2022-05-14 19:41:32 +02:00
2025-06-08 12:42:13 +02:00
const result = await database . query ( 'SELECT backgroundImage FROM users WHERE id = ?' , [ user . id ] ) ;
2022-05-14 19:41:32 +02:00
if ( result . length === 0 ) throw new BoxError ( BoxError . NOT _FOUND , 'User not found' ) ;
return result [ 0 ] . backgroundImage ;
}
2025-06-08 12:42:13 +02:00
async function setBackgroundImage ( user , backgroundImage ) {
assert . strictEqual ( typeof user , 'object' ) ;
2022-05-15 12:14:17 +02:00
assert ( Buffer . isBuffer ( backgroundImage ) || backgroundImage === null ) ;
2022-05-14 19:41:32 +02:00
2025-06-08 12:42:13 +02:00
const result = await database . query ( 'UPDATE users SET backgroundImage=? WHERE id = ?' , [ backgroundImage , user . id ] ) ;
2022-05-14 19:41:32 +02:00
if ( result . length === 0 ) throw new BoxError ( BoxError . NOT _FOUND , 'User not found' ) ;
}
2023-08-03 08:11:42 +05:30
2024-12-11 18:24:20 +01:00
async function setNotificationConfig ( user , notificationConfig , auditSource ) {
assert . strictEqual ( typeof user , 'object' ) ;
assert ( Array . isArray ( notificationConfig ) ) ;
assert ( auditSource && typeof auditSource === 'object' ) ;
2024-12-11 20:44:46 +01:00
await update ( user , { notificationConfig } , auditSource ) ;
2024-12-11 18:24:20 +01:00
}
2024-12-04 09:48:25 +01:00
async function resetSources ( ) {
2024-01-13 11:27:08 +01:00
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 } ;
}
2025-10-08 20:11:55 +02:00