2018-05-19 20:40:07 +00:00
/* global WIKI */
const bcrypt = require ( 'bcryptjs-then' )
const _ = require ( 'lodash' )
const tfa = require ( 'node-2fa' )
2018-10-08 04:17:31 +00:00
const jwt = require ( 'jsonwebtoken' )
2018-05-19 20:40:07 +00:00
const Model = require ( 'objection' ) . Model
2018-12-21 04:02:17 +00:00
const validate = require ( 'validate.js' )
2020-08-22 23:37:49 +00:00
const qr = require ( 'qr-image' )
2018-05-19 20:40:07 +00:00
const bcryptRegexp = /^\$2[ayb]\$[0-9]{2}\$[A-Za-z0-9./]{53}$/
/ * *
* Users model
* /
module . exports = class User extends Model {
static get tableName ( ) { return 'users' }
static get jsonSchema ( ) {
return {
type : 'object' ,
2019-04-22 01:43:33 +00:00
required : [ 'email' ] ,
2018-05-19 20:40:07 +00:00
properties : {
id : { type : 'integer' } ,
email : { type : 'string' , format : 'email' } ,
name : { type : 'string' , minLength : 1 , maxLength : 255 } ,
2019-04-22 01:43:33 +00:00
providerId : { type : 'string' } ,
2018-05-19 20:40:07 +00:00
password : { type : 'string' } ,
tfaIsActive : { type : 'boolean' , default : false } ,
2020-08-30 18:18:22 +00:00
tfaSecret : { type : [ 'string' , null ] } ,
2018-05-28 18:46:55 +00:00
jobTitle : { type : 'string' } ,
location : { type : 'string' } ,
pictureUrl : { type : 'string' } ,
2018-12-15 22:15:13 +00:00
isSystem : { type : 'boolean' } ,
2018-12-22 21:18:16 +00:00
isActive : { type : 'boolean' } ,
isVerified : { type : 'boolean' } ,
2018-05-19 20:40:07 +00:00
createdAt : { type : 'string' } ,
updatedAt : { type : 'string' }
}
}
}
static get relationMappings ( ) {
return {
groups : {
relation : Model . ManyToManyRelation ,
2018-05-20 22:50:51 +00:00
modelClass : require ( './groups' ) ,
2018-05-19 20:40:07 +00:00
join : {
from : 'users.id' ,
through : {
from : 'userGroups.userId' ,
to : 'userGroups.groupId'
} ,
to : 'groups.id'
}
2018-07-22 04:29:39 +00:00
} ,
provider : {
relation : Model . BelongsToOneRelation ,
modelClass : require ( './authentication' ) ,
join : {
from : 'users.providerKey' ,
to : 'authentication.key'
}
} ,
defaultEditor : {
relation : Model . BelongsToOneRelation ,
modelClass : require ( './editors' ) ,
join : {
from : 'users.editorKey' ,
to : 'editors.key'
}
} ,
locale : {
relation : Model . BelongsToOneRelation ,
modelClass : require ( './locales' ) ,
join : {
from : 'users.localeCode' ,
to : 'locales.code'
}
2018-05-19 20:40:07 +00:00
}
}
}
async $beforeUpdate ( opt , context ) {
await super . $beforeUpdate ( opt , context )
this . updatedAt = new Date ( ) . toISOString ( )
if ( ! ( opt . patch && this . password === undefined ) ) {
await this . generateHash ( )
}
}
async $beforeInsert ( context ) {
await super . $beforeInsert ( context )
this . createdAt = new Date ( ) . toISOString ( )
this . updatedAt = new Date ( ) . toISOString ( )
await this . generateHash ( )
}
2019-01-12 23:33:30 +00:00
// ------------------------------------------------
// Instance Methods
// ------------------------------------------------
2018-05-19 20:40:07 +00:00
async generateHash ( ) {
if ( this . password ) {
if ( bcryptRegexp . test ( this . password ) ) { return }
this . password = await bcrypt . hash ( this . password , 12 )
}
}
async verifyPassword ( pwd ) {
2018-05-20 22:50:51 +00:00
if ( await bcrypt . compare ( pwd , this . password ) === true ) {
2018-05-19 20:40:07 +00:00
return true
} else {
throw new WIKI . Error . AuthLoginFailed ( )
}
}
2020-08-22 23:37:49 +00:00
async generateTFA ( ) {
2018-05-19 20:40:07 +00:00
let tfaInfo = tfa . generateSecret ( {
2020-08-22 23:37:49 +00:00
name : WIKI . config . title ,
account : this . email
2018-05-19 20:40:07 +00:00
} )
2020-08-22 23:37:49 +00:00
await WIKI . models . users . query ( ) . findById ( this . id ) . patch ( {
tfaIsActive : false ,
2018-05-19 20:40:07 +00:00
tfaSecret : tfaInfo . secret
} )
2020-09-10 00:10:51 +00:00
const safeTitle = WIKI . config . title . replace ( /[\s-.,=!@#$%?&*()+[\]{}/\\;<>]/g , '' )
return qr . imageSync ( ` otpauth://totp/ ${ safeTitle } : ${ this . email } ?secret= ${ tfaInfo . secret } ` , { type : 'svg' } )
2020-08-22 23:37:49 +00:00
}
async enableTFA ( ) {
return WIKI . models . users . query ( ) . findById ( this . id ) . patch ( {
tfaIsActive : true
} )
2018-05-19 20:40:07 +00:00
}
async disableTFA ( ) {
return this . $query . patch ( {
tfaIsActive : false ,
tfaSecret : ''
} )
}
2020-08-22 23:37:49 +00:00
verifyTFA ( code ) {
2018-05-19 20:40:07 +00:00
let result = tfa . verifyToken ( this . tfaSecret , code )
return ( result && _ . has ( result , 'delta' ) && result . delta === 0 )
}
2019-01-12 23:33:30 +00:00
getGlobalPermissions ( ) {
return _ . uniq ( _ . flatten ( _ . map ( this . groups , 'permissions' ) ) )
}
getGroups ( ) {
return _ . uniq ( _ . map ( this . groups , 'id' ) )
2019-01-07 03:03:34 +00:00
}
2019-01-12 23:33:30 +00:00
// ------------------------------------------------
// Model Methods
// ------------------------------------------------
2019-04-22 01:43:33 +00:00
static async processProfile ( { profile , providerKey } ) {
const provider = _ . get ( WIKI . auth . strategies , providerKey , { } )
2020-08-30 05:36:17 +00:00
provider . info = _ . find ( WIKI . data . authentication , [ 'key' , provider . stategyKey ] )
2019-04-22 01:43:33 +00:00
// Find existing user
let user = await WIKI . models . users . query ( ) . findOne ( {
2019-04-28 18:11:27 +00:00
providerId : _ . toString ( profile . id ) ,
2019-04-22 01:43:33 +00:00
providerKey
} )
// Parse email
2018-05-19 20:40:07 +00:00
let primaryEmail = ''
if ( _ . isArray ( profile . emails ) ) {
2019-04-21 06:04:00 +00:00
const e = _ . find ( profile . emails , [ 'primary' , true ] )
2018-05-19 20:40:07 +00:00
primaryEmail = ( e ) ? e . value : _ . first ( profile . emails ) . value
2020-10-03 20:23:58 +00:00
} else if ( _ . isArray ( profile . email ) ) {
2020-10-03 21:11:34 +00:00
primaryEmail = _ . first ( _ . flattenDeep ( [ profile . email ] ) )
2018-05-19 20:40:07 +00:00
} else if ( _ . isString ( profile . email ) && profile . email . length > 5 ) {
primaryEmail = profile . email
} else if ( _ . isString ( profile . mail ) && profile . mail . length > 5 ) {
primaryEmail = profile . mail
} else if ( profile . user && profile . user . email && profile . user . email . length > 5 ) {
primaryEmail = profile . user . email
} else {
2019-04-22 01:43:33 +00:00
throw new Error ( 'Missing or invalid email address from profile.' )
2018-05-19 20:40:07 +00:00
}
primaryEmail = _ . toLower ( primaryEmail )
2019-08-11 02:14:53 +00:00
// Find pending social user
if ( ! user ) {
user = await WIKI . models . users . query ( ) . findOne ( {
email : primaryEmail ,
providerId : null ,
providerKey
} )
if ( user ) {
user = await user . $query ( ) . patchAndFetch ( {
providerId : _ . toString ( profile . id )
} )
}
}
2019-04-22 01:43:33 +00:00
// Parse display name
let displayName = ''
if ( _ . isString ( profile . displayName ) && profile . displayName . length > 0 ) {
displayName = profile . displayName
} else if ( _ . isString ( profile . name ) && profile . name . length > 0 ) {
displayName = profile . name
} else {
displayName = primaryEmail . split ( '@' ) [ 0 ]
}
2020-09-08 00:02:33 +00:00
// Parse picture URL / Data
let pictureUrl = ''
if ( profile . picture && Buffer . isBuffer ( profile . picture ) ) {
pictureUrl = 'internal'
} else {
pictureUrl = _ . truncate ( _ . get ( profile , 'picture' , _ . get ( user , 'pictureUrl' , null ) ) , {
length : 255 ,
omission : ''
} )
}
2019-04-22 01:43:33 +00:00
// Update existing user
2018-05-19 20:40:07 +00:00
if ( user ) {
2019-04-22 01:43:33 +00:00
if ( ! user . isActive ) {
throw new WIKI . Error . AuthAccountBanned ( )
}
if ( user . isSystem ) {
throw new Error ( 'This is a system reserved account and cannot be used.' )
}
user = await user . $query ( ) . patchAndFetch ( {
2018-05-19 20:40:07 +00:00
email : primaryEmail ,
2019-04-22 01:43:33 +00:00
name : displayName ,
pictureUrl : pictureUrl
} )
2020-09-08 00:02:33 +00:00
if ( pictureUrl === 'internal' ) {
await WIKI . models . users . updateUserAvatarData ( user . id , profile . picture )
}
2019-04-22 01:43:33 +00:00
return user
}
// Self-registration
if ( provider . selfRegistration ) {
// Check if email domain is whitelisted
if ( _ . get ( provider , 'domainWhitelist' , [ ] ) . length > 0 ) {
const emailDomain = _ . last ( primaryEmail . split ( '@' ) )
if ( ! _ . includes ( provider . domainWhitelist , emailDomain ) ) {
throw new WIKI . Error . AuthRegistrationDomainUnauthorized ( )
}
}
// Create account
user = await WIKI . models . users . query ( ) . insertAndFetch ( {
providerKey : providerKey ,
2019-04-28 18:11:27 +00:00
providerId : _ . toString ( profile . id ) ,
2019-04-22 01:43:33 +00:00
email : primaryEmail ,
name : displayName ,
pictureUrl : pictureUrl ,
localeCode : WIKI . config . lang . code ,
defaultEditor : 'markdown' ,
tfaIsActive : false ,
isSystem : false ,
isActive : true ,
isVerified : true
2018-05-19 20:40:07 +00:00
} )
2019-04-22 01:43:33 +00:00
// Assign to group(s)
if ( provider . autoEnrollGroups . length > 0 ) {
await user . $relatedQuery ( 'groups' ) . relate ( provider . autoEnrollGroups )
}
2020-09-08 00:02:33 +00:00
if ( pictureUrl === 'internal' ) {
await WIKI . models . users . updateUserAvatarData ( user . id , profile . picture )
}
2019-04-22 01:43:33 +00:00
return user
2018-05-19 20:40:07 +00:00
}
2019-04-22 01:43:33 +00:00
throw new Error ( 'You are not authorized to login.' )
2018-05-19 20:40:07 +00:00
}
2020-09-02 00:01:25 +00:00
/ * *
* Login a user
* /
2018-05-19 20:40:07 +00:00
static async login ( opts , context ) {
2018-06-17 15:12:11 +00:00
if ( _ . has ( WIKI . auth . strategies , opts . strategy ) ) {
2020-08-30 05:36:17 +00:00
const selStrategy = _ . get ( WIKI . auth . strategies , opts . strategy )
2020-09-05 22:33:15 +00:00
if ( ! selStrategy . isEnabled ) {
throw new WIKI . Error . AuthProviderInvalid ( )
}
2020-08-30 05:36:17 +00:00
const strInfo = _ . find ( WIKI . data . authentication , [ 'key' , selStrategy . strategyKey ] )
2019-04-21 06:04:00 +00:00
// Inject form user/pass
if ( strInfo . useForm ) {
_ . set ( context . req , 'body.email' , opts . username )
_ . set ( context . req , 'body.password' , opts . password )
2020-09-08 00:02:33 +00:00
_ . set ( context . req . params , 'strategy' , opts . strategy )
2019-04-21 06:04:00 +00:00
}
2018-05-19 20:40:07 +00:00
// Authenticate
return new Promise ( ( resolve , reject ) => {
2020-08-30 05:36:17 +00:00
WIKI . auth . passport . authenticate ( selStrategy . strategyKey , {
2019-04-21 06:04:00 +00:00
session : ! strInfo . useForm ,
2019-04-22 01:43:33 +00:00
scope : strInfo . scopes ? strInfo . scopes : null
2019-04-21 06:04:00 +00:00
} , async ( err , user , info ) => {
2018-05-19 20:40:07 +00:00
if ( err ) { return reject ( err ) }
if ( ! user ) { return reject ( new WIKI . Error . AuthLoginFailed ( ) ) }
2020-08-22 23:37:49 +00:00
try {
2020-08-30 05:36:17 +00:00
const resp = await WIKI . models . users . afterLoginChecks ( user , context , {
skipTFA : ! strInfo . useForm ,
skipChangePwd : ! strInfo . useForm
} )
2020-08-22 23:37:49 +00:00
resolve ( resp )
} catch ( err ) {
reject ( err )
2020-07-19 19:13:35 +00:00
}
2018-05-19 20:40:07 +00:00
} ) ( context . req , context . res , ( ) => { } )
} )
} else {
throw new WIKI . Error . AuthProviderInvalid ( )
}
}
2020-09-02 00:01:25 +00:00
/ * *
* Perform post - login checks
* /
2020-08-22 23:37:49 +00:00
static async afterLoginChecks ( user , context , { skipTFA , skipChangePwd } = { skipTFA : false , skipChangePwd : false } ) {
// Get redirect target
user . groups = await user . $relatedQuery ( 'groups' ) . select ( 'groups.id' , 'permissions' , 'redirectOnLogin' )
let redirect = '/'
if ( user . groups && user . groups . length > 0 ) {
2020-10-03 21:11:34 +00:00
for ( const grp of user . groups ) {
if ( ! _ . isEmpty ( grp . redirectOnLogin ) && grp . redirectOnLogin !== '/' ) {
redirect = grp . redirectOnLogin
break
}
}
2020-08-22 23:37:49 +00:00
}
2020-10-03 21:11:34 +00:00
console . info ( redirect )
2020-08-22 23:37:49 +00:00
// Is 2FA required?
if ( ! skipTFA ) {
if ( user . tfaIsActive && user . tfaSecret ) {
try {
const tfaToken = await WIKI . models . userKeys . generateToken ( {
kind : 'tfa' ,
userId : user . id
} )
return {
mustProvideTFA : true ,
continuationToken : tfaToken ,
redirect
}
} catch ( errc ) {
WIKI . logger . warn ( errc )
throw new WIKI . Error . AuthGenericError ( )
}
} else if ( WIKI . config . auth . enforce2FA || ( user . tfaIsActive && ! user . tfaSecret ) ) {
try {
const tfaQRImage = await user . generateTFA ( )
const tfaToken = await WIKI . models . userKeys . generateToken ( {
kind : 'tfaSetup' ,
userId : user . id
} )
return {
mustSetupTFA : true ,
continuationToken : tfaToken ,
tfaQRImage ,
redirect
}
} catch ( errc ) {
WIKI . logger . warn ( errc )
throw new WIKI . Error . AuthGenericError ( )
}
}
}
// Must Change Password?
if ( ! skipChangePwd && user . mustChangePwd ) {
try {
const pwdChangeToken = await WIKI . models . userKeys . generateToken ( {
kind : 'changePwd' ,
userId : user . id
} )
return {
mustChangePwd : true ,
continuationToken : pwdChangeToken ,
redirect
}
} catch ( errc ) {
WIKI . logger . warn ( errc )
throw new WIKI . Error . AuthGenericError ( )
}
}
return new Promise ( ( resolve , reject ) => {
context . req . login ( user , { session : false } , async errc => {
if ( errc ) { return reject ( errc ) }
const jwtToken = await WIKI . models . users . refreshToken ( user )
resolve ( { jwt : jwtToken . token , redirect } )
} )
} )
}
2020-09-02 00:01:25 +00:00
/ * *
* Generate a new token for a user
* /
2018-10-08 04:17:31 +00:00
static async refreshToken ( user ) {
if ( _ . isSafeInteger ( user ) ) {
2020-01-25 00:20:53 +00:00
user = await WIKI . models . users . query ( ) . findById ( user ) . withGraphFetched ( 'groups' ) . modifyGraph ( 'groups' , builder => {
2019-01-12 23:33:30 +00:00
builder . select ( 'groups.id' , 'permissions' )
} )
2018-10-08 04:17:31 +00:00
if ( ! user ) {
WIKI . logger . warn ( ` Failed to refresh token for user ${ user } : Not found. ` )
throw new WIKI . Error . AuthGenericError ( )
}
2020-06-24 22:15:36 +00:00
if ( ! user . isActive ) {
WIKI . logger . warn ( ` Failed to refresh token for user ${ user } : Inactive. ` )
throw new WIKI . Error . AuthAccountBanned ( )
}
2019-03-19 19:15:40 +00:00
} else if ( _ . isNil ( user . groups ) ) {
2020-01-25 00:20:53 +00:00
user . groups = await user . $relatedQuery ( 'groups' ) . select ( 'groups.id' , 'permissions' )
2018-10-08 04:17:31 +00:00
}
2019-01-12 23:33:30 +00:00
2020-04-05 22:51:48 +00:00
// Update Last Login Date
2020-04-13 00:36:18 +00:00
// -> Bypass Objection.js to avoid updating the updatedAt field
await WIKI . models . knex ( 'users' ) . where ( 'id' , user . id ) . update ( { lastLoginAt : new Date ( ) . toISOString ( ) } )
2020-04-05 22:51:48 +00:00
2018-10-08 04:17:31 +00:00
return {
token : jwt . sign ( {
id : user . id ,
email : user . email ,
name : user . name ,
2020-05-03 04:38:02 +00:00
av : user . pictureUrl ,
tz : user . timezone ,
lc : user . localeCode ,
df : user . dateFormat ,
ap : user . appearance ,
// defaultEditor: user.defaultEditor,
2019-01-12 23:33:30 +00:00
permissions : user . getGlobalPermissions ( ) ,
groups : user . getGroups ( )
2018-12-03 02:42:43 +00:00
} , {
key : WIKI . config . certs . private ,
passphrase : WIKI . config . sessionSecret
} , {
algorithm : 'RS256' ,
2019-01-07 03:03:34 +00:00
expiresIn : WIKI . config . auth . tokenExpiration ,
audience : WIKI . config . auth . audience ,
2018-10-08 04:17:31 +00:00
issuer : 'urn:wiki.js'
} ) ,
user
}
}
2020-09-02 00:01:25 +00:00
/ * *
* Verify a TFA login
* /
2020-08-22 23:37:49 +00:00
static async loginTFA ( { securityCode , continuationToken , setup } , context ) {
if ( securityCode . length === 6 && continuationToken . length > 1 ) {
const user = await WIKI . models . userKeys . validateToken ( {
kind : setup ? 'tfaSetup' : 'tfa' ,
token : continuationToken ,
skipDelete : setup
} )
if ( user ) {
if ( user . verifyTFA ( securityCode ) ) {
if ( setup ) {
await user . enableTFA ( )
2018-05-19 20:40:07 +00:00
}
2020-08-22 23:37:49 +00:00
return WIKI . models . users . afterLoginChecks ( user , context , { skipTFA : true } )
} else {
throw new WIKI . Error . AuthTFAFailed ( )
2018-05-19 20:40:07 +00:00
}
}
}
throw new WIKI . Error . AuthTFAInvalid ( )
}
2018-12-17 05:51:52 +00:00
2019-08-25 02:19:35 +00:00
/ * *
* Change Password from a Mandatory Password Change after Login
* /
static async loginChangePassword ( { continuationToken , newPassword } , context ) {
if ( ! newPassword || newPassword . length < 6 ) {
throw new WIKI . Error . InputInvalid ( 'Password must be at least 6 characters!' )
}
const usr = await WIKI . models . userKeys . validateToken ( {
kind : 'changePwd' ,
token : continuationToken
} )
if ( usr ) {
await WIKI . models . users . query ( ) . patch ( {
password : newPassword ,
mustChangePwd : false
} ) . findById ( usr . id )
return new Promise ( ( resolve , reject ) => {
context . req . logIn ( usr , { session : false } , async err => {
if ( err ) { return reject ( err ) }
const jwtToken = await WIKI . models . users . refreshToken ( usr )
resolve ( { jwt : jwtToken . token } )
} )
} )
} else {
throw new WIKI . Error . UserNotFound ( )
}
}
2020-08-31 01:46:55 +00:00
/ * *
* Send a password reset request
* /
static async loginForgotPassword ( { email } , context ) {
const usr = await WIKI . models . users . query ( ) . where ( {
email ,
providerKey : 'local'
} ) . first ( )
if ( ! usr ) {
WIKI . logger . debug ( ` Password reset attempt on nonexistant local account ${ email } : [DISCARDED] ` )
return
}
const resetToken = await WIKI . models . userKeys . generateToken ( {
userId : usr . id ,
kind : 'resetPwd'
} )
await WIKI . mail . send ( {
template : 'accountResetPwd' ,
to : email ,
subject : ` Password Reset Request ` ,
data : {
preheadertext : ` A password reset was requested for ${ WIKI . config . title } ` ,
title : ` A password reset was requested for ${ WIKI . config . title } ` ,
content : ` Click the button below to reset your password. If you didn't request this password reset, simply discard this email. ` ,
buttonLink : ` ${ WIKI . config . host } /login-reset/ ${ resetToken } ` ,
buttonText : 'Reset Password'
} ,
text : ` A password reset was requested for wiki ${ WIKI . config . title } . Open the following link to proceed: ${ WIKI . config . host } /login-reset/ ${ resetToken } `
} )
}
2019-08-17 22:29:58 +00:00
/ * *
* Create a new user
*
* @ param { Object } param0 User Fields
* /
2019-07-29 04:50:03 +00:00
static async createNewUser ( { providerKey , email , passwordRaw , name , groups , mustChangePassword , sendWelcomeEmail } ) {
// Input sanitization
email = _ . toLower ( email )
// Input validation
2019-08-11 02:14:53 +00:00
let validation = null
if ( providerKey === 'local' ) {
validation = validate ( {
email ,
passwordRaw ,
name
} , {
email : {
email : true ,
length : {
maximum : 255
}
2019-07-29 04:50:03 +00:00
} ,
2019-08-11 02:14:53 +00:00
passwordRaw : {
presence : {
allowEmpty : false
} ,
length : {
minimum : 6
}
} ,
name : {
presence : {
allowEmpty : false
} ,
length : {
minimum : 2 ,
maximum : 255
}
2019-07-29 04:50:03 +00:00
}
2019-08-11 02:14:53 +00:00
} , { format : 'flat' } )
} else {
validation = validate ( {
email ,
name
} , {
email : {
email : true ,
length : {
maximum : 255
}
2019-07-29 04:50:03 +00:00
} ,
2019-08-11 02:14:53 +00:00
name : {
presence : {
allowEmpty : false
} ,
length : {
minimum : 2 ,
maximum : 255
}
2019-07-29 04:50:03 +00:00
}
2019-08-11 02:14:53 +00:00
} , { format : 'flat' } )
}
2019-07-29 04:50:03 +00:00
if ( validation && validation . length > 0 ) {
throw new WIKI . Error . InputInvalid ( validation [ 0 ] )
}
// Check if email already exists
const usr = await WIKI . models . users . query ( ) . findOne ( { email , providerKey } )
if ( ! usr ) {
// Create the account
2019-08-11 02:14:53 +00:00
let newUsrData = {
providerKey ,
2019-07-29 04:50:03 +00:00
email ,
name ,
locale : 'en' ,
defaultEditor : 'markdown' ,
tfaIsActive : false ,
isSystem : false ,
isActive : true ,
isVerified : true ,
2019-08-11 02:14:53 +00:00
mustChangePwd : false
}
if ( providerKey === ` local ` ) {
newUsrData . password = passwordRaw
newUsrData . mustChangePwd = ( mustChangePassword === true )
}
const newUsr = await WIKI . models . users . query ( ) . insert ( newUsrData )
2019-07-29 04:50:03 +00:00
// Assign to group(s)
if ( groups . length > 0 ) {
await newUsr . $relatedQuery ( 'groups' ) . relate ( groups )
}
if ( sendWelcomeEmail ) {
// Send welcome email
await WIKI . mail . send ( {
template : 'accountWelcome' ,
to : email ,
subject : ` Welcome to the wiki ${ WIKI . config . title } ` ,
data : {
preheadertext : ` You've been invited to the wiki ${ WIKI . config . title } ` ,
title : ` You've been invited to the wiki ${ WIKI . config . title } ` ,
content : ` Click the button below to access the wiki. ` ,
buttonLink : ` ${ WIKI . config . host } /login ` ,
buttonText : 'Login'
} ,
text : ` You've been invited to the wiki ${ WIKI . config . title } : ${ WIKI . config . host } /login `
} )
}
} else {
throw new WIKI . Error . AuthAccountAlreadyExists ( )
}
}
2019-08-17 22:29:58 +00:00
/ * *
* Update an existing user
*
* @ param { Object } param0 User ID and fields to update
* /
2020-05-03 04:38:02 +00:00
static async updateUser ( { id , email , name , newPassword , groups , location , jobTitle , timezone , dateFormat , appearance } ) {
2019-08-17 22:29:58 +00:00
const usr = await WIKI . models . users . query ( ) . findById ( id )
if ( usr ) {
let usrData = { }
if ( ! _ . isEmpty ( email ) && email !== usr . email ) {
const dupUsr = await WIKI . models . users . query ( ) . select ( 'id' ) . where ( {
email ,
providerKey : usr . providerKey
2019-11-17 03:32:55 +00:00
} ) . first ( )
2019-08-17 22:29:58 +00:00
if ( dupUsr ) {
throw new WIKI . Error . AuthAccountAlreadyExists ( )
}
2020-09-09 23:59:46 +00:00
usrData . email = _ . toLower ( email )
2019-08-17 22:29:58 +00:00
}
if ( ! _ . isEmpty ( name ) && name !== usr . name ) {
usrData . name = _ . trim ( name )
}
if ( ! _ . isEmpty ( newPassword ) ) {
if ( newPassword . length < 6 ) {
throw new WIKI . Error . InputInvalid ( 'Password must be at least 6 characters!' )
}
usrData . password = newPassword
}
2019-08-25 02:19:35 +00:00
if ( _ . isArray ( groups ) ) {
2019-08-17 22:29:58 +00:00
const usrGroupsRaw = await usr . $relatedQuery ( 'groups' )
const usrGroups = _ . map ( usrGroupsRaw , 'id' )
// Relate added groups
const addUsrGroups = _ . difference ( groups , usrGroups )
for ( const grp of addUsrGroups ) {
await usr . $relatedQuery ( 'groups' ) . relate ( grp )
}
// Unrelate removed groups
const remUsrGroups = _ . difference ( usrGroups , groups )
for ( const grp of remUsrGroups ) {
await usr . $relatedQuery ( 'groups' ) . unrelate ( ) . where ( 'groupId' , grp )
}
}
if ( ! _ . isEmpty ( location ) && location !== usr . location ) {
usrData . location = _ . trim ( location )
}
if ( ! _ . isEmpty ( jobTitle ) && jobTitle !== usr . jobTitle ) {
usrData . jobTitle = _ . trim ( jobTitle )
}
if ( ! _ . isEmpty ( timezone ) && timezone !== usr . timezone ) {
usrData . timezone = timezone
}
2020-05-03 04:38:02 +00:00
if ( ! _ . isNil ( dateFormat ) && dateFormat !== usr . dateFormat ) {
usrData . dateFormat = dateFormat
}
if ( ! _ . isNil ( appearance ) && appearance !== usr . appearance ) {
usrData . appearance = appearance
}
2019-08-17 22:29:58 +00:00
await WIKI . models . users . query ( ) . patch ( usrData ) . findById ( id )
} else {
2020-03-15 16:06:45 +00:00
throw new WIKI . Error . UserNotFound ( )
}
}
/ * *
* Delete a User
*
* @ param { * } id User ID
* /
2020-05-30 20:34:09 +00:00
static async deleteUser ( id , replaceId ) {
2020-03-15 16:06:45 +00:00
const usr = await WIKI . models . users . query ( ) . findById ( id )
if ( usr ) {
2020-05-30 20:34:09 +00:00
await WIKI . models . assets . query ( ) . patch ( { authorId : replaceId } ) . where ( 'authorId' , id )
await WIKI . models . comments . query ( ) . patch ( { authorId : replaceId } ) . where ( 'authorId' , id )
await WIKI . models . pageHistory . query ( ) . patch ( { authorId : replaceId } ) . where ( 'authorId' , id )
await WIKI . models . pages . query ( ) . patch ( { authorId : replaceId } ) . where ( 'authorId' , id )
await WIKI . models . pages . query ( ) . patch ( { creatorId : replaceId } ) . where ( 'creatorId' , id )
2020-03-15 16:06:45 +00:00
await WIKI . models . userKeys . query ( ) . delete ( ) . where ( 'userId' , id )
await WIKI . models . users . query ( ) . deleteById ( id )
} else {
2019-08-17 22:29:58 +00:00
throw new WIKI . Error . UserNotFound ( )
}
}
/ * *
* Register a new user ( client - side registration )
*
* @ param { Object } param0 User fields
* @ param { Object } context GraphQL Context
* /
2019-01-01 06:40:31 +00:00
static async register ( { email , password , name , verify = false , bypassChecks = false } , context ) {
2018-12-21 04:02:17 +00:00
const localStrg = await WIKI . models . authentication . getStrategy ( 'local' )
// Check if self-registration is enabled
2019-01-01 06:40:31 +00:00
if ( localStrg . selfRegistration || bypassChecks ) {
// Input sanitization
email = _ . toLower ( email )
2018-12-21 04:02:17 +00:00
// Input validation
const validation = validate ( {
2018-12-17 05:51:52 +00:00
email ,
password ,
2018-12-21 04:02:17 +00:00
name
} , {
email : {
email : true ,
length : {
maximum : 255
}
} ,
password : {
presence : {
allowEmpty : false
} ,
length : {
minimum : 6
}
} ,
name : {
presence : {
allowEmpty : false
} ,
length : {
minimum : 2 ,
maximum : 255
}
2019-03-19 19:15:40 +00:00
}
2018-12-21 04:02:17 +00:00
} , { format : 'flat' } )
if ( validation && validation . length > 0 ) {
throw new WIKI . Error . InputInvalid ( validation [ 0 ] )
}
// Check if email domain is whitelisted
2019-01-01 06:40:31 +00:00
if ( _ . get ( localStrg , 'domainWhitelist.v' , [ ] ) . length > 0 && ! bypassChecks ) {
2018-12-21 04:02:17 +00:00
const emailDomain = _ . last ( email . split ( '@' ) )
if ( ! _ . includes ( localStrg . domainWhitelist . v , emailDomain ) ) {
throw new WIKI . Error . AuthRegistrationDomainUnauthorized ( )
}
}
// Check if email already exists
const usr = await WIKI . models . users . query ( ) . findOne ( { email , providerKey : 'local' } )
if ( ! usr ) {
// Create the account
2018-12-24 06:03:10 +00:00
const newUsr = await WIKI . models . users . query ( ) . insert ( {
2018-12-21 04:02:17 +00:00
provider : 'local' ,
email ,
name ,
password ,
locale : 'en' ,
defaultEditor : 'markdown' ,
tfaIsActive : false ,
2018-12-22 21:18:16 +00:00
isSystem : false ,
isActive : true ,
isVerified : false
} )
2019-06-08 17:30:07 +00:00
// Assign to group(s)
if ( _ . get ( localStrg , 'autoEnrollGroups.v' , [ ] ) . length > 0 ) {
await newUsr . $relatedQuery ( 'groups' ) . relate ( localStrg . autoEnrollGroups . v )
}
2019-01-01 06:40:31 +00:00
if ( verify ) {
// Create verification token
const verificationToken = await WIKI . models . userKeys . generateToken ( {
kind : 'verify' ,
userId : newUsr . id
} )
2018-12-24 06:03:10 +00:00
2019-01-01 06:40:31 +00:00
// Send verification email
await WIKI . mail . send ( {
template : 'accountVerify' ,
to : email ,
subject : 'Verify your account' ,
data : {
preheadertext : 'Verify your account in order to gain access to the wiki.' ,
title : 'Verify your account' ,
content : 'Click the button below in order to verify your account and gain access to the wiki.' ,
buttonLink : ` ${ WIKI . config . host } /verify/ ${ verificationToken } ` ,
buttonText : 'Verify'
} ,
text : ` You must open the following link in your browser to verify your account and gain access to the wiki: ${ WIKI . config . host } /verify/ ${ verificationToken } `
} )
}
2018-12-21 04:02:17 +00:00
return true
} else {
throw new WIKI . Error . AuthAccountAlreadyExists ( )
}
2018-12-17 05:51:52 +00:00
} else {
2018-12-21 04:02:17 +00:00
throw new WIKI . Error . AuthRegistrationDisabled ( )
2018-12-17 05:51:52 +00:00
}
}
2019-01-07 03:03:34 +00:00
2020-09-02 00:01:25 +00:00
/ * *
* Logout the current user
* /
static async logout ( context ) {
if ( ! context . req . user || context . req . user . id === 2 ) {
return '/'
}
const usr = await WIKI . models . users . query ( ) . findById ( context . req . user . id ) . select ( 'providerKey' )
const provider = _ . find ( WIKI . auth . strategies , [ 'key' , usr . providerKey ] )
return provider . logout ? provider . logout ( provider . config ) : '/'
}
2019-01-07 03:03:34 +00:00
static async getGuestUser ( ) {
2020-01-26 04:29:46 +00:00
const user = await WIKI . models . users . query ( ) . findById ( 2 ) . withGraphJoined ( 'groups' ) . modifyGraph ( 'groups' , builder => {
2019-01-12 23:33:30 +00:00
builder . select ( 'groups.id' , 'permissions' )
} )
if ( ! user ) {
WIKI . logger . error ( 'CRITICAL ERROR: Guest user is missing!' )
process . exit ( 1 )
}
2019-03-19 19:15:40 +00:00
user . permissions = user . getGlobalPermissions ( )
2019-01-07 03:03:34 +00:00
return user
}
2019-10-15 03:44:37 +00:00
static async getRootUser ( ) {
let user = await WIKI . models . users . query ( ) . findById ( 1 )
if ( ! user ) {
WIKI . logger . error ( 'CRITICAL ERROR: Root Administrator user is missing!' )
process . exit ( 1 )
}
user . permissions = [ 'manage:system' ]
return user
}
2020-09-08 00:02:33 +00:00
/ * *
* Add / Update User Avatar Data
* /
static async updateUserAvatarData ( userId , data ) {
try {
WIKI . logger . debug ( ` Updating user ${ userId } avatar data... ` )
if ( data . length > 1024 * 1024 ) {
throw new Error ( 'Avatar image filesize is too large. 1MB max.' )
}
const existing = await WIKI . models . knex ( 'userAvatars' ) . select ( 'id' ) . where ( 'id' , userId ) . first ( )
if ( existing ) {
await WIKI . models . knex ( 'userAvatars' ) . where ( {
id : userId
} ) . update ( {
data
} )
} else {
await WIKI . models . knex ( 'userAvatars' ) . insert ( {
id : userId ,
data
} )
}
} catch ( err ) {
WIKI . logger . warn ( ` Failed to process binary thumbnail data for user ${ userId } : ${ err . message } ` )
}
}
static async getUserAvatarData ( userId ) {
try {
const usrData = await WIKI . models . knex ( 'userAvatars' ) . where ( 'id' , userId ) . first ( )
if ( usrData ) {
return usrData . data
} else {
return null
}
} catch ( err ) {
WIKI . logger . warn ( ` Failed to process binary thumbnail data for user ${ userId } ` )
}
}
2018-05-19 20:40:07 +00:00
}