2017-08-02 21:24:01 +00:00
import path from 'path'
import cprog from 'child_process'
import crypto from 'crypto'
2017-08-02 22:35:10 +00:00
import notp from 'notp'
import base32 from 'thirty-two'
2017-09-22 20:59:43 +00:00
import uuidV1 from 'uuid/v1'
2017-11-30 21:13:14 +00:00
import fs from 'fs-extra'
2017-08-02 21:24:01 +00:00
2017-12-01 11:35:47 +00:00
import config from '../../scripts/load-config'
import http from '../../scripts/http'
import models from './models'
import emailer from './emailer'
2017-08-02 21:24:01 +00:00
const emailRe = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
2017-09-10 09:42:12 +00:00
// Fork a bcrypt process to hash and compare passwords
2017-08-02 21:24:01 +00:00
function bcryptTask ( data ) {
return new Promise ( ( resolve , reject ) => {
let proc = cprog . fork ( path . join ( _ _dirname , '../../scripts' , 'bcrypt.js' ) )
let done = false
proc . send ( data . task + ' ' + JSON . stringify ( data ) )
proc . on ( 'message' , ( chunk ) => {
if ( chunk == null ) return reject ( new Error ( 'No response' ) )
let line = chunk . toString ( ) . trim ( )
done = true
if ( line === 'error' ) {
return reject ( new Error ( line ) )
}
if ( line === 'true' || line === 'false' ) {
return resolve ( line === 'true' )
}
resolve ( line )
} )
proc . on ( 'exit' , ( ) => {
if ( ! done ) {
reject ( new Error ( 'No response' ) )
}
} )
} )
}
2017-09-10 09:42:12 +00:00
// Make sure an object contains the keys specified in `required`
2017-08-02 22:35:10 +00:00
function keysAvailable ( object , required ) {
let found = true
for ( let i in required ) {
let key = required [ i ]
if ( object [ key ] == null ) {
found = false
}
}
return found
}
2017-09-10 09:42:12 +00:00
// Clean up the donation responses for ease of use
2017-08-31 17:24:38 +00:00
async function cleanUpDonation ( obj , mcOnly , timeframe ) {
if ( timeframe && new Date ( obj . created _at ) . getTime ( ) < timeframe ) {
return null
}
let user
if ( obj . user _id ) {
user = await API . User . get ( obj . user _id )
}
let result = {
trackId : obj . id ,
amount : obj . amount ,
donated : obj . created _at
}
if ( user ) {
result . name = user . display _name
}
2017-09-01 12:02:20 +00:00
let sources = obj . source . split ( ',' )
2017-08-31 17:24:38 +00:00
for ( let i in sources ) {
if ( sources [ i ] . indexOf ( 'mcu:' ) === 0 ) {
let mcu = sources [ i ] . split ( ':' ) [ 1 ]
if ( mcu . match ( /^([\w_]{2,16})$/i ) ) {
result . minecraft _username = mcu
}
}
}
if ( ! result . minecraft _username && mcOnly ) return null
return result
}
2017-08-27 17:47:52 +00:00
let txnStore = [ ]
2017-08-02 21:24:01 +00:00
const API = {
2017-08-02 22:35:10 +00:00
Hash : ( len ) => {
return crypto . randomBytes ( len ) . toString ( 'hex' )
} ,
2017-08-28 15:42:16 +00:00
/* ppp - Posts Per Page; dcount - Post Count; page - number of current page */
Pagination : ( ppp , dcount , page ) => {
if ( ! ppp ) ppp = 5
if ( ! dcount ) return null
let pageCount = Math . ceil ( dcount / ppp )
if ( page > pageCount ) page = pageCount
let offset = ( page - 1 ) * ppp
return {
page : page ,
perPage : ppp ,
pages : pageCount ,
offset : offset ,
total : dcount
}
} ,
2017-08-02 21:24:01 +00:00
User : {
get : async function ( identifier ) {
let scope = 'id'
if ( typeof identifier === 'string' ) {
scope = 'username'
if ( identifier . indexOf ( '@' ) !== - 1 ) {
scope = 'email'
2017-11-18 08:59:49 +00:00
} else if ( identifier . length === 36 && identifier . match ( /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i ) ) {
scope = 'uuid'
2017-08-02 21:24:01 +00:00
}
} else if ( typeof identifier === 'object' ) {
if ( identifier . id != null ) {
identifier = identifier . id
} else if ( identifier . username ) {
scope = 'username'
identifier = identifier . username
} else {
return null
}
}
let user = await models . User . query ( ) . where ( scope , identifier )
if ( ! user . length ) return null
return user [ 0 ]
} ,
2017-08-02 22:35:10 +00:00
ensureObject : async function ( user , fieldsPresent = [ 'id' ] ) {
if ( typeof user !== 'object' || ! keysAvailable ( user , fieldsPresent ) ) {
2017-08-24 16:23:03 +00:00
return API . User . get ( user )
2017-08-02 21:24:01 +00:00
}
if ( user . id ) {
return user
}
return null
} ,
2017-08-24 18:36:40 +00:00
socialStatus : async function ( user ) {
user = await API . User . ensureObject ( user , [ 'password' ] )
if ( ! user ) return null
let external = await models . External . query ( ) . orderBy ( 'created_at' , 'asc' ) . where ( 'user_id' , user . id )
let enabled = { }
for ( let i in external ) {
let ext = external [ i ]
enabled [ ext . service ] = true
}
let accountSourceIsExternal = user . password === null || user . password === ''
let obj = {
enabled : enabled ,
password : ! accountSourceIsExternal
}
if ( accountSourceIsExternal ) {
obj . source = external [ 0 ] . service
}
return obj
} ,
update : async function ( user , data ) {
user = await API . User . ensureObject ( user )
2017-11-30 21:13:14 +00:00
if ( ! user ) throw new Error ( 'No such user.' )
2017-08-24 18:36:40 +00:00
data = Object . assign ( {
updated _at : new Date ( )
} , data )
return models . User . query ( ) . patchAndFetchById ( user . id , data )
} ,
2017-08-25 16:42:30 +00:00
changeAvatar : async function ( user , fileName ) {
user = await API . User . ensureObject ( user , [ 'avatar_file' ] )
let uploadsDir = path . join ( _ _dirname , '../../' , 'usercontent' , 'images' )
let pathOf = path . join ( uploadsDir , fileName )
2017-11-30 21:13:14 +00:00
if ( ! await fs . exists ( pathOf ) ) {
throw new Error ( 'No such file' )
2017-08-25 16:42:30 +00:00
}
// Delete previous upload
if ( user . avatar _file != null ) {
let file = path . join ( uploadsDir , user . avatar _file )
2017-11-30 21:13:14 +00:00
if ( await fs . exists ( file ) ) {
await fs . unlink ( file )
2017-08-25 16:42:30 +00:00
}
}
await API . User . update ( user , { avatar _file : fileName } )
2017-11-30 21:13:14 +00:00
return fileName
2017-08-25 16:42:30 +00:00
} ,
removeAvatar : async function ( user ) {
user = await API . User . ensureObject ( user , [ 'avatar_file' ] )
let uploadsDir = path . join ( _ _dirname , '../../' , 'usercontent' , 'images' )
if ( ! user . avatar _file ) return { }
let file = path . join ( uploadsDir , user . avatar _file )
2017-11-30 21:13:14 +00:00
if ( await fs . exists ( file ) ) {
2017-11-30 21:45:21 +00:00
await fs . unlink ( file )
2017-08-25 16:42:30 +00:00
}
return API . User . update ( user , { avatar _file : null } )
} ,
2017-08-27 12:41:44 +00:00
getBanStatus : async function ( field , ip = false ) {
let bans
if ( ip === true ) {
bans = await models . Ban . query ( ) . where ( 'associated_ip' , field )
} else {
bans = await models . Ban . query ( ) . where ( 'user_id' , field )
}
let bansActive = [ ]
for ( let i in bans ) {
let ban = bans [ i ]
// Check expiry
if ( ban . expires _at && new Date ( ban . expires _at ) . getTime ( ) < Date . now ( ) ) continue
let banInfo = {
banned : ban . created _at ,
reason : ban . reason ,
expiry : ban . expires _at
}
bansActive . push ( banInfo )
}
return bansActive
} ,
2017-08-02 21:24:01 +00:00
Login : {
password : async function ( user , password ) {
2017-08-24 18:36:40 +00:00
user = await API . User . ensureObject ( user , [ 'password' ] )
2017-08-02 21:24:01 +00:00
if ( ! user . password ) return false
return bcryptTask ( { task : 'compare' , password : password , hash : user . password } )
} ,
activationToken : async function ( token ) {
2017-11-30 21:13:14 +00:00
let getToken = await models . Token . query ( ) . where ( 'token' , token ) . andWhere ( 'type' , 1 )
2017-08-02 21:24:01 +00:00
if ( ! getToken || ! getToken . length ) return false
2017-11-30 21:26:41 +00:00
getToken = getToken [ 0 ]
if ( getToken . expires _at && new Date ( getToken . expires _at ) . getTime ( ) < Date . now ( ) ) return false
let user = await API . User . get ( getToken . user _id )
2017-08-02 21:24:01 +00:00
if ( ! user ) return false
await models . User . query ( ) . patchAndFetchById ( user . id , { activated : 1 } )
2017-11-30 21:26:41 +00:00
await models . Token . query ( ) . delete ( ) . where ( 'id' , getToken . id )
2017-08-02 21:24:01 +00:00
return true
2017-08-02 22:35:10 +00:00
} ,
totpTokenRequired : async function ( user ) {
let getToken = await models . TotpToken . query ( ) . where ( 'user_id' , user . id )
if ( ! getToken || ! getToken . length ) return false
if ( getToken [ 0 ] . activated !== 1 ) return false
return true
} ,
totpCheck : async function ( user , code , emerg ) {
user = await API . User . ensureObject ( user )
let getToken = await models . TotpToken . query ( ) . where ( 'user_id' , user . id )
if ( ! getToken || ! getToken . length ) return false
getToken = getToken [ 0 ]
if ( emerg ) {
if ( emerg === getToken . recovery _code ) {
return true
}
return false
}
let login = notp . totp . verify ( code , getToken . token , { } )
if ( login ) {
if ( login . delta !== 0 ) {
return false
}
if ( getToken . activated !== 1 ) {
// TODO: Send an email including the recovery code to the user
await models . TotpToken . query ( ) . patchAndFetchById ( getToken . id , { activated : true } )
}
return true
}
return false
} ,
purgeTotp : async function ( user , password ) {
user = await API . User . ensureObject ( user , [ 'password' ] )
let pwmatch = await API . User . Login . password ( user , password )
if ( ! pwmatch ) return false
// TODO: Inform user via email
await models . TotpToken . query ( ) . delete ( ) . where ( 'user_id' , user . id )
return true
} ,
totpAquire : async function ( user ) {
2017-08-23 22:25:52 +00:00
user = await API . User . ensureObject ( user , [ 'password' ] )
2017-08-03 12:57:17 +00:00
// Do not allow totp for users who have registered using an external service
if ( ! user . password || user . password === '' ) return null
// Get existing tokens for the user and delete them if found
2017-08-02 22:35:10 +00:00
let getToken = await models . TotpToken . query ( ) . where ( 'user_id' , user . id )
if ( getToken && getToken . length ) {
await models . TotpToken . query ( ) . delete ( ) . where ( 'user_id' , user . id )
}
let newToken = {
user _id : user . id ,
token : API . Hash ( 16 ) ,
recovery _code : API . Hash ( 8 ) ,
2017-08-03 12:57:17 +00:00
activated : 0 ,
2017-08-02 22:35:10 +00:00
created _at : new Date ( )
}
let hashed = base32 . encode ( newToken . token )
let domain = 'icynet.eu'
await models . TotpToken . query ( ) . insert ( newToken )
let uri = encodeURIComponent ( 'otpauth://totp/' + user . username + '@' + domain + '?secret=' + hashed )
return uri
2017-08-02 21:24:01 +00:00
}
} ,
Register : {
hashPassword : async function ( password ) {
return bcryptTask ( { task : 'hash' , password : password } )
} ,
validateEmail : ( email ) => {
return emailRe . test ( email )
} ,
newAccount : async function ( regdata ) {
2017-08-24 13:42:57 +00:00
let email = config . email && config . email . enabled
2017-08-02 21:24:01 +00:00
let data = Object . assign ( regdata , {
2017-08-24 13:42:57 +00:00
created _at : new Date ( ) ,
2017-08-24 18:36:40 +00:00
updated _at : new Date ( ) ,
2017-09-22 20:59:43 +00:00
uuid : uuidV1 ( ) ,
2017-08-24 13:42:57 +00:00
activated : email ? 0 : 1
2017-08-02 21:24:01 +00:00
} )
let userTest = await API . User . get ( regdata . username )
if ( userTest ) {
2017-11-30 21:13:14 +00:00
throw new Error ( 'This username is already taken!' )
2017-08-02 21:24:01 +00:00
}
let emailTest = await API . User . get ( regdata . email )
if ( emailTest ) {
2017-11-30 21:13:14 +00:00
throw new Error ( 'This email address is already registered!' )
2017-08-02 21:24:01 +00:00
}
// Create user
let user = await models . User . query ( ) . insert ( data )
2017-12-07 15:37:36 +00:00
if ( email ) {
await API . User . Register . activationEmail ( user , true )
}
return user
} ,
activationEmail : async function ( user , deleteOnFail = false ) {
2017-08-02 21:24:01 +00:00
// Activation token
2017-08-02 22:35:10 +00:00
let activationToken = API . Hash ( 16 )
2017-12-07 15:37:36 +00:00
2017-08-02 21:24:01 +00:00
await models . Token . query ( ) . insert ( {
expires _at : new Date ( Date . now ( ) + 86400000 ) , // 1 day
token : activationToken ,
user _id : user . id ,
type : 1
} )
2017-08-24 13:42:57 +00:00
console . debug ( 'Activation token:' , activationToken )
2017-12-07 15:37:36 +00:00
// Send Activation Email
try {
let em = await emailer . pushMail ( 'activate' , user . email , {
domain : config . server . domain ,
display _name : user . display _name ,
activation _token : activationToken
} )
console . debug ( em )
} catch ( e ) {
console . error ( e )
if ( deleteOnFail ) {
2017-10-28 09:09:55 +00:00
await models . User . query ( ) . delete ( ) . where ( 'id' , user . id )
}
2017-12-07 15:37:36 +00:00
throw new Error ( 'Invalid email address!' )
2017-08-24 13:42:57 +00:00
}
2017-12-07 15:37:36 +00:00
return true
2017-11-30 21:13:14 +00:00
}
} ,
Reset : {
reset : async function ( email , passRequired = true ) {
let emailEnabled = config . email && config . email . enabled
if ( ! emailEnabled ) throw new Error ( 'Cannot reset password.' )
let user = await API . User . get ( email )
if ( ! user ) throw new Error ( 'This email address does not match any user in our database.' )
if ( ! user . password && passRequired ) throw new Error ( 'The user associated with this email address has used an external website to log in, thus the password cannot be reset.' )
2017-11-30 21:26:41 +00:00
let recentTokens = await models . Token . query ( ) . where ( 'user_id' , user . id ) . andWhere ( 'expires_at' , '>' , new Date ( ) ) . andWhere ( 'type' , 2 )
if ( recentTokens . length >= 2 ) {
throw new Error ( 'You\'ve made too many reset requests recently. Please slow down.' )
}
2017-11-30 21:13:14 +00:00
let resetToken = API . Hash ( 16 )
await models . Token . query ( ) . insert ( {
expires _at : new Date ( Date . now ( ) + 86400000 ) , // 1 day
token : resetToken ,
user _id : user . id ,
type : 2
} )
// Send Reset Email
console . debug ( 'Reset token:' , resetToken )
if ( email ) {
try {
let em = await emailer . pushMail ( 'reset_password' , user . email , {
domain : config . server . domain ,
display _name : user . display _name ,
reset _token : resetToken
} )
console . debug ( em )
} catch ( e ) {
console . error ( e )
throw new Error ( 'Invalid email address!' )
}
}
return resetToken
} ,
resetToken : async function ( token ) {
let getToken = await models . Token . query ( ) . where ( 'token' , token ) . andWhere ( 'type' , 2 )
if ( ! getToken || ! getToken . length ) return null
2017-11-30 21:26:41 +00:00
getToken = getToken [ 0 ]
if ( getToken . expires _at && new Date ( getToken . expires _at ) . getTime ( ) < Date . now ( ) ) return null
let user = await API . User . get ( getToken . user _id )
2017-11-30 21:13:14 +00:00
if ( ! user ) return null
return user
} ,
changePassword : async function ( user , password , token ) {
let hashed = await API . User . Register . hashPassword ( password )
await models . User . query ( ) . patchAndFetchById ( user . id , { password : hashed , updated _at : new Date ( ) } )
await models . Token . query ( ) . delete ( ) . where ( 'token' , token )
return true
2017-08-02 21:24:01 +00:00
}
2017-08-26 09:47:37 +00:00
} ,
OAuth2 : {
getUserAuthorizations : async function ( user ) {
user = await API . User . ensureObject ( user )
let auths = await models . OAuth2AuthorizedClient . query ( ) . where ( 'user_id' , user . id )
let nicelist = [ ]
for ( let i in auths ) {
let auth = auths [ i ]
let client = await models . OAuth2Client . query ( ) . where ( 'id' , auth . client _id )
if ( ! client . length ) continue
client = client [ 0 ]
let obj = {
id : client . id ,
title : client . title ,
description : client . description ,
url : client . url ,
icon : client . icon ,
scope : client . scope . split ( ' ' ) ,
created _at : auth . created _at ,
expires _at : auth . expires _at
}
nicelist . push ( obj )
}
return nicelist
} ,
removeUserAuthorization : async function ( user , clientId ) {
user = await API . User . ensureObject ( user )
let auth = await models . OAuth2AuthorizedClient . query ( ) . where ( 'user_id' , user . id ) . andWhere ( 'client_id' , clientId )
if ( ! auth . length ) return false
2017-08-26 10:02:17 +00:00
await models . OAuth2AccessToken . query ( ) . delete ( ) . where ( 'client_id' , clientId ) . andWhere ( 'user_id' , user . id )
await models . OAuth2RefreshToken . query ( ) . delete ( ) . where ( 'client_id' , clientId ) . andWhere ( 'user_id' , user . id )
2017-08-26 09:47:37 +00:00
for ( let i in auth ) {
await models . OAuth2AuthorizedClient . query ( ) . delete ( ) . where ( 'id' , auth [ i ] . id )
}
return true
}
2017-08-02 21:24:01 +00:00
}
2017-08-27 17:47:52 +00:00
} ,
Payment : {
handleIPN : async function ( body ) {
let sandboxed = body . test _ipn === '1'
let url = 'https://ipnpb.' + ( sandboxed ? 'sandbox.' : '' ) + 'paypal.com/cgi-bin/webscr'
console . debug ( 'Incoming payment' )
let verification = await http . POST ( url , { } , Object . assign ( {
cmd : '_notify-validate'
} , body ) )
if ( verification !== 'VERIFIED' ) return null
2017-08-27 17:59:41 +00:00
// Ignore the adding of non-on-site donations
if ( body . item _name && config . donations . name && body . item _name !== config . donations . name ) {
return true
}
2017-08-27 17:47:52 +00:00
if ( sandboxed ) {
console . debug ( 'Sandboxed payment:' , body )
} else {
console . debug ( 'IPN Verified Notification:' , body )
}
if ( body . txn _id ) {
if ( txnStore . indexOf ( body . txn _id ) !== - 1 ) return true
txnStore . push ( body . txn _id )
}
let user
let source = [ ]
if ( sandboxed ) {
source . push ( 'virtual' )
}
// TODO: add hooks
let custom = body . custom . split ( ',' )
for ( let i in custom ) {
let str = custom [ i ]
if ( str . indexOf ( 'userid:' ) === 0 ) {
body . user _id = parseInt ( str . split ( ':' ) [ 1 ] )
} else if ( str . indexOf ( 'mcu:' ) === 0 ) {
source . push ( 'mcu:' + str . split ( ':' ) [ 1 ] )
}
}
if ( body . user _id != null ) {
user = await API . User . get ( body . user _id )
} else if ( body . payer _email != null ) {
user = await API . User . get ( body . payer _email )
}
let donation = {
user _id : user ? user . id : null ,
amount : ( body . mc _gross || body . payment _gross || 'Unknown' ) + ' ' + ( body . mc _currency || 'EUR' ) ,
2017-09-01 12:02:20 +00:00
source : source . join ( ',' ) ,
2017-08-27 17:47:52 +00:00
note : body . memo || '' ,
read : 0 ,
created _at : new Date ( body . payment _date )
}
console . log ( 'Server receieved a successful PayPal IPN message.' )
return models . Donation . query ( ) . insert ( donation )
} ,
userContributions : async function ( user ) {
user = await API . User . ensureObject ( user )
2017-08-31 17:24:38 +00:00
let dbq = await models . Donation . query ( ) . orderBy ( 'created_at' , 'desc' ) . where ( 'user_id' , user . id )
2017-08-27 17:47:52 +00:00
let contribs = [ ]
for ( let i in dbq ) {
2017-08-31 17:24:38 +00:00
contribs . push ( await cleanUpDonation ( dbq [ i ] ) )
}
2017-08-27 17:47:52 +00:00
2017-08-31 17:24:38 +00:00
return contribs
} ,
allContributions : async function ( count , mcOnly , timeframe = 0 ) {
let dbq = await models . Donation . query ( ) . orderBy ( 'created_at' , 'desc' ) . limit ( count )
let contribs = [ ]
2017-08-27 17:47:52 +00:00
2017-08-31 17:24:38 +00:00
for ( let i in dbq ) {
let obj = await cleanUpDonation ( dbq [ i ] , mcOnly , timeframe )
if ( ! obj ) continue
2017-08-27 17:47:52 +00:00
contribs . push ( obj )
}
return contribs
}
2017-08-02 21:24:01 +00:00
}
}
module . exports = API