@ -9,7 +9,6 @@ import qr from 'qr-image'
import bcrypt from 'bcryptjs'
import bcrypt from 'bcryptjs'
import { Group } from './groups.mjs'
import { Group } from './groups.mjs'
import { Locale } from './locales.mjs'
/ * *
/ * *
* Users model
* Users model
@ -73,35 +72,39 @@ export class User extends Model {
// Instance Methods
// Instance Methods
// ------------------------------------------------
// ------------------------------------------------
async generateTFA ( ) {
async generateTFA ( strategyId , siteId ) {
let tfaInfo = tfa . generateSecret ( {
WIKI . logger . debug ( ` Generating new TFA secret for user ${ this . id } ... ` )
name : WIKI . config . title ,
const site = WIKI . sites [ siteId ] ? ? WIKI . sites [ 0 ] ? ? { config : { title : 'Wiki' } }
const tfaInfo = tfa . generateSecret ( {
name : site . config . title ,
account : this . email
account : this . email
} )
} )
await WIKI . db . users . query ( ) . findById ( this . id ) . patch ( {
this . auth [ strategyId ] . tfaSecret = tfaInfo . secret
tfaIsActive : false ,
this . auth [ strategyId ] . tfaIsActive = false
tfaSecret : tfaInfo . secret
await this . $query ( ) . patch ( {
auth : this . auth
} )
} )
const safeTitle = WIKI . config . title . replace ( /[\s-.,=!@#$%?&*()+[\]{}/\\;<>]/g , '' )
const safeTitle = site . config . title . replace ( /[\s-.,=!@#$%?&*()+[\]{}/\\;<>]/g , '' )
return qr . imageSync ( ` otpauth://totp/ ${ safeTitle } : ${ this . email } ?secret= ${ tfaInfo . secret } ` , { type : 'svg' } )
return qr . imageSync ( ` otpauth://totp/ ${ safeTitle } : ${ this . email } ?secret= ${ tfaInfo . secret } ` , { type : 'svg' } )
}
}
async enableTFA ( ) {
async enableTFA ( strategyId ) {
return WIKI . db . users . query ( ) . findById ( this . id ) . patch ( {
this . auth [ strategyId ] . tfaIsActive = true
tfaIsActive : true
return this . $query ( ) . patch ( {
auth : this . auth
} )
} )
}
}
async disableTFA ( ) {
async disableTFA ( strategyId ) {
return this . $query . patch ( {
this . auth [ strategyId ] . tfaIsActive = false
return this . $query ( ) . patch ( {
tfaIsActive : false ,
tfaIsActive : false ,
tfaSecret : ''
tfaSecret : ''
} )
} )
}
}
verifyTFA ( code ) {
verifyTFA ( strategyId , code ) {
let result = tfa . verifyToken ( this . tfaSecret , code )
return tfa . verifyToken ( this . auth [ strategyId ] . tfaSecret , code ) ? . delta === 0
return ( result && has ( result , 'delta' ) && result . delta === 0 )
}
}
getPermissions ( ) {
getPermissions ( ) {
@ -250,9 +253,9 @@ export class User extends Model {
/ * *
/ * *
* Login a user
* Login a user
* /
* /
static async login ( opts , context ) {
static async login ( { strategyId , siteId , username , password } , context ) {
if ( has ( WIKI . auth . strategies , opts. strategy) ) {
if ( has ( WIKI . auth . strategies , strategyId ) ) {
const selStrategy = get( WIKI. auth . strategies , opts . strategy )
const selStrategy = WIKI. auth . strategies [strategyId ]
if ( ! selStrategy . isEnabled ) {
if ( ! selStrategy . isEnabled ) {
throw new WIKI . Error . AuthProviderInvalid ( )
throw new WIKI . Error . AuthProviderInvalid ( )
}
}
@ -261,9 +264,9 @@ export class User extends Model {
// Inject form user/pass
// Inject form user/pass
if ( strInfo . useForm ) {
if ( strInfo . useForm ) {
set ( context . req , 'body.email' , opts. username)
set ( context . req , 'body.email' , username)
set ( context . req , 'body.password' , opts. password)
set ( context . req , 'body.password' , password)
set ( context . req . params , 'strategy' , opts. strategy)
set ( context . req . params , 'strategy' , strategyId )
}
}
// Authenticate
// Authenticate
@ -277,6 +280,7 @@ export class User extends Model {
try {
try {
const resp = await WIKI . db . users . afterLoginChecks ( user , selStrategy . id , context , {
const resp = await WIKI . db . users . afterLoginChecks ( user , selStrategy . id , context , {
siteId ,
skipTFA : ! strInfo . useForm ,
skipTFA : ! strInfo . useForm ,
skipChangePwd : ! strInfo . useForm
skipChangePwd : ! strInfo . useForm
} )
} )
@ -294,7 +298,12 @@ export class User extends Model {
/ * *
/ * *
* Perform post - login checks
* Perform post - login checks
* /
* /
static async afterLoginChecks ( user , strategyId , context , { skipTFA , skipChangePwd } = { skipTFA : false , skipChangePwd : false } ) {
static async afterLoginChecks ( user , strategyId , context , { siteId , skipTFA , skipChangePwd } = { skipTFA : false , skipChangePwd : false , siteId : null } ) {
const str = WIKI . auth . strategies [ strategyId ]
if ( ! str ) {
throw new Error ( 'ERR_INVALID_STRATEGY' )
}
// Get redirect target
// Get redirect target
user . groups = await user . $relatedQuery ( 'groups' ) . select ( 'groups.id' , 'permissions' , 'redirectOnLogin' )
user . groups = await user . $relatedQuery ( 'groups' ) . select ( 'groups.id' , 'permissions' , 'redirectOnLogin' )
let redirect = '/'
let redirect = '/'
@ -312,14 +321,14 @@ export class User extends Model {
// Is 2FA required?
// Is 2FA required?
if ( ! skipTFA ) {
if ( ! skipTFA ) {
if ( authStr . tfa Required && authStr . tfaSecret ) {
if ( authStr . tfa IsActive && authStr . tfaSecret ) {
try {
try {
const tfaToken = await WIKI . db . userKeys . generateToken ( {
const tfaToken = await WIKI . db . userKeys . generateToken ( {
kind : 'tfa' ,
kind : 'tfa' ,
userId : user . id
userId : user . id
} )
} )
return {
return {
mustProvideTFA: true ,
nextAction: 'provideTfa' ,
continuationToken : tfaToken ,
continuationToken : tfaToken ,
redirect
redirect
}
}
@ -327,15 +336,15 @@ export class User extends Model {
WIKI . logger . warn ( errc )
WIKI . logger . warn ( errc )
throw new WIKI . Error . AuthGenericError ( )
throw new WIKI . Error . AuthGenericError ( )
}
}
} else if ( WIKI. config . auth . enforce2FA || ( authStr . tfaIsActive && ! authStr . tfaSecret ) ) {
} else if ( str. config ? . enforceTfa || authStr . tfaRequired ) {
try {
try {
const tfaQRImage = await user . generateTFA ( )
const tfaQRImage = await user . generateTFA ( strategyId , siteId )
const tfaToken = await WIKI . db . userKeys . generateToken ( {
const tfaToken = await WIKI . db . userKeys . generateToken ( {
kind : 'tfaSetup' ,
kind : 'tfaSetup' ,
userId : user . id
userId : user . id
} )
} )
return {
return {
mustSetupTFA: true ,
nextAction: 'setupTfa' ,
continuationToken : tfaToken ,
continuationToken : tfaToken ,
tfaQRImage ,
tfaQRImage ,
redirect
redirect
@ -356,7 +365,7 @@ export class User extends Model {
} )
} )
return {
return {
mustChangePwd: true ,
nextAction: 'changePassword' ,
continuationToken : pwdChangeToken ,
continuationToken : pwdChangeToken ,
redirect
redirect
}
}
@ -370,7 +379,11 @@ export class User extends Model {
context . req . login ( user , { session : false } , async errc => {
context . req . login ( user , { session : false } , async errc => {
if ( errc ) { return reject ( errc ) }
if ( errc ) { return reject ( errc ) }
const jwtToken = await WIKI . db . users . refreshToken ( user , strategyId )
const jwtToken = await WIKI . db . users . refreshToken ( user , strategyId )
resolve ( { jwt : jwtToken . token , redirect } )
resolve ( {
nextAction : 'redirect' ,
jwt : jwtToken . token ,
redirect
} )
} )
} )
} )
} )
}
}
@ -420,19 +433,19 @@ export class User extends Model {
/ * *
/ * *
* Verify a TFA login
* Verify a TFA login
* /
* /
static async loginTFA ( { s ecurityCode, continuationToken , setup } , context ) {
static async loginTFA ( { s trategyId, siteId , s ecurityCode, continuationToken , setup } , context ) {
if ( securityCode . length === 6 && continuationToken . length > 1 ) {
if ( securityCode . length === 6 && continuationToken . length > 1 ) {
const user = await WIKI . db . userKeys . validateToken ( {
const { user } = await WIKI . db . userKeys . validateToken ( {
kind : setup ? 'tfaSetup' : 'tfa' ,
kind : setup ? 'tfaSetup' : 'tfa' ,
token : continuationToken ,
token : continuationToken ,
skipDelete : setup
skipDelete : setup
} )
} )
if ( user ) {
if ( user ) {
if ( user . verifyTFA ( s ecurityCode) ) {
if ( user . verifyTFA ( s trategyId, s ecurityCode) ) {
if ( setup ) {
if ( setup ) {
await user . enableTFA ( )
await user . enableTFA ( strategyId )
}
}
return WIKI . db . users . afterLoginChecks ( user , context, { skipTFA : true } )
return WIKI . db . users . afterLoginChecks ( user , strategyId, context, { siteId , skipTFA : true } )
} else {
} else {
throw new WIKI . Error . AuthTFAFailed ( )
throw new WIKI . Error . AuthTFAFailed ( )
}
}
@ -508,7 +521,14 @@ export class User extends Model {
*
*
* @ param { Object } param0 User Fields
* @ param { Object } param0 User Fields
* /
* /
static async createNewUser ( { email , password , name , groups , mustChangePassword = false , sendWelcomeEmail = false } ) {
static async createNewUser ( { email , password , name , groups , userInitiated = false , mustChangePassword = false , sendWelcomeEmail = false } ) {
const localAuth = await WIKI . db . authentication . getStrategy ( 'local' )
// Check if self-registration is enabled
if ( userInitiated && ! localAuth . registration ) {
throw new Error ( 'ERR_REGISTRATION_DISABLED' )
}
// Input sanitization
// Input sanitization
email = email . toLowerCase ( ) . trim ( )
email = email . toLowerCase ( ) . trim ( )
@ -547,14 +567,23 @@ export class User extends Model {
throw new Error ( ` ERR_INVALID_INPUT: ${ validation [ 0 ] } ` )
throw new Error ( ` ERR_INVALID_INPUT: ${ validation [ 0 ] } ` )
}
}
// Check if email address is allowed
if ( userInitiated && localAuth . allowedEmailRegex ) {
const emailCheckRgx = new RegExp ( localAuth . allowedEmailRegex , 'i' )
if ( ! emailCheckRgx . test ( email ) ) {
throw new Error ( 'ERR_EMAIL_ADDRESS_NOT_ALLOWED' )
}
}
// Check if email already exists
// Check if email already exists
const usr = await WIKI . db . users . query ( ) . findOne ( { email } )
const usr = await WIKI . db . users . query ( ) . findOne ( { email } )
if ( usr ) {
if ( usr ) {
throw new Error ( 'ERR_ACCOUNT_ALREADY_EXIST' )
throw new Error ( 'ERR_ACCOUNT_ALREADY_EXIST' )
}
}
WIKI . logger . debug ( ` Creating new user account for ${ email } ... ` )
// Create the account
// Create the account
const localAuth = await WIKI . db . authentication . getStrategy ( 'local' )
const newUsr = await WIKI . db . users . query ( ) . insert ( {
const newUsr = await WIKI . db . users . query ( ) . insert ( {
email ,
email ,
name ,
name ,
@ -583,14 +612,41 @@ export class User extends Model {
dateFormat : WIKI . config . userDefaults . dateFormat || 'YYYY-MM-DD' ,
dateFormat : WIKI . config . userDefaults . dateFormat || 'YYYY-MM-DD' ,
timeFormat : WIKI . config . userDefaults . timeFormat || '12h'
timeFormat : WIKI . config . userDefaults . timeFormat || '12h'
}
}
} )
} ) . returning ( '*' )
// Assign to group(s)
// Assign to group(s)
if ( groups . length > 0 ) {
const groupsToEnroll = [ WIKI . data . systemIds . usersGroupId ]
await newUsr . $relatedQuery ( 'groups' ) . relate ( groups )
if ( groups ? . length > 0 ) {
groupsToEnroll . push ( ... groups )
}
if ( userInitiated && localAuth . autoEnrollGroups ? . length > 0 ) {
groupsToEnroll . push ( ... localAuth . autoEnrollGroups )
}
}
await newUsr . $relatedQuery ( 'groups' ) . relate ( uniq ( groupsToEnroll ) )
// Verification Email
if ( userInitiated && localAuth . config ? . emailValidation ) {
// Create verification token
const verificationToken = await WIKI . db . userKeys . generateToken ( {
kind : 'verify' ,
userId : newUsr . id
} )
if ( sendWelcomeEmail ) {
// 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 } `
} )
} else if ( sendWelcomeEmail ) {
// Send welcome email
// Send welcome email
await WIKI . mail . send ( {
await WIKI . mail . send ( {
template : 'accountWelcome' ,
template : 'accountWelcome' ,
@ -606,6 +662,10 @@ export class User extends Model {
text : ` You've been invited to the wiki ${ WIKI . config . title } : ${ WIKI . config . host } /login `
text : ` You've been invited to the wiki ${ WIKI . config . title } : ${ WIKI . config . host } /login `
} )
} )
}
}
WIKI . logger . debug ( ` Created new user account for ${ email } successfully. ` )
return newUsr
}
}
/ * *
/ * *
@ -680,113 +740,6 @@ export class User extends Model {
}
}
}
}
/ * *
* Register a new user ( client - side registration )
*
* @ param { Object } param0 User fields
* @ param { Object } context GraphQL Context
* /
static async register ( { email , password , name , verify = false , bypassChecks = false } , context ) {
const localStrg = await WIKI . db . authentication . getStrategy ( 'local' )
// Check if self-registration is enabled
if ( localStrg . selfRegistration || bypassChecks ) {
// Input sanitization
email = email . toLowerCase ( )
// Input validation
const validation = validate ( {
email ,
password ,
name
} , {
email : {
email : true ,
length : {
maximum : 255
}
} ,
password : {
presence : {
allowEmpty : false
} ,
length : {
minimum : 6
}
} ,
name : {
presence : {
allowEmpty : false
} ,
length : {
minimum : 2 ,
maximum : 255
}
}
} , { format : 'flat' } )
if ( validation && validation . length > 0 ) {
throw new WIKI . Error . InputInvalid ( validation [ 0 ] )
}
// Check if email domain is whitelisted
if ( get ( localStrg , 'domainWhitelist.v' , [ ] ) . length > 0 && ! bypassChecks ) {
const emailDomain = last ( email . split ( '@' ) )
if ( ! localStrg . domainWhitelist . v . includes ( emailDomain ) ) {
throw new WIKI . Error . AuthRegistrationDomainUnauthorized ( )
}
}
// Check if email already exists
const usr = await WIKI . db . users . query ( ) . findOne ( { email , providerKey : 'local' } )
if ( ! usr ) {
// Create the account
const newUsr = await WIKI . db . users . query ( ) . insert ( {
provider : 'local' ,
email ,
name ,
password ,
locale : 'en' ,
defaultEditor : 'markdown' ,
tfaIsActive : false ,
isSystem : false ,
isActive : true ,
isVerified : false
} )
// Assign to group(s)
if ( get ( localStrg , 'autoEnrollGroups.v' , [ ] ) . length > 0 ) {
await newUsr . $relatedQuery ( 'groups' ) . relate ( localStrg . autoEnrollGroups . v )
}
if ( verify ) {
// Create verification token
const verificationToken = await WIKI . db . userKeys . generateToken ( {
kind : 'verify' ,
userId : newUsr . id
} )
// 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 } `
} )
}
return true
} else {
throw new WIKI . Error . AuthAccountAlreadyExists ( )
}
} else {
throw new WIKI . Error . AuthRegistrationDisabled ( )
}
}
/ * *
/ * *
* Logout the current user
* Logout the current user
* /
* /