2017-12-01 11:35:47 +00:00
import qs from 'querystring'
import oauth from 'oauth-libre'
2020-05-28 18:30:21 +00:00
import { v1 as uuidV1 } from 'uuid'
2017-12-01 11:35:47 +00:00
import crypto from 'crypto'
2017-08-03 12:57:17 +00:00
import config from '../../scripts/load-config'
2020-12-13 14:36:07 +00:00
import { httpGET } from '../../scripts/http'
import * as models from './models'
2017-11-30 21:45:21 +00:00
import Image from './image'
2020-12-13 14:36:07 +00:00
import { User , Hash } from './index'
2017-08-03 12:57:17 +00:00
2017-11-23 16:18:26 +00:00
const userFields = [ 'username' , 'email' , 'avatar_file' , 'display_name' , 'ip_address' ]
2017-09-09 11:15:11 +00:00
2017-08-03 12:57:17 +00:00
let twitterApp
let discordApp
2020-12-13 14:36:07 +00:00
export class Common {
// Generate a hash based on the current session
static stateGenerator ( req ) {
const sessionCrypto = req . session . id + ':' + config . server . session _secret
return crypto . createHash ( 'sha256' ) . update ( sessionCrypto ) . digest ( 'hex' )
}
// Find an user with an external ID
static async getExternal ( service , identifier ) {
let extr = await models . External . query ( ) . where ( 'service' , service ) . andWhere ( 'identifier' , identifier )
if ( ! extr || ! extr . length ) return null
extr = extr [ 0 ]
extr . user = null
2017-08-03 12:57:17 +00:00
2020-12-13 14:36:07 +00:00
if ( extr . user _id !== null ) {
const user = await User . get ( extr . user _id )
if ( user ) {
extr . user = user
2017-08-03 12:57:17 +00:00
}
2020-12-13 14:36:07 +00:00
}
2017-08-03 12:57:17 +00:00
2020-12-13 14:36:07 +00:00
return extr
}
2017-09-10 09:42:12 +00:00
2020-12-13 14:36:07 +00:00
// Get user ban status
static async getBan ( user , ipAddress ) {
const banList = await User . getBanStatus ( ipAddress || user . id , ipAddress != null )
return banList
}
2017-11-23 16:26:53 +00:00
2020-12-13 14:36:07 +00:00
// Create a new `external` instance for a user
static async new ( service , identifier , user ) {
const data = {
user _id : user . id ,
service : service ,
identifier : identifier ,
created _at : new Date ( )
}
2017-11-23 16:26:53 +00:00
2020-12-13 14:36:07 +00:00
await models . External . query ( ) . insert ( data )
return true
}
2017-11-23 16:26:53 +00:00
2020-12-13 14:36:07 +00:00
// Create a new user
static async newUser ( service , identifier , data ) {
if ( config . external . registrations !== true ) throw new Error ( 'Registrations from third-party websites are not allowed.' )
const udataLimited = Object . assign ( {
activated : 1 ,
created _at : new Date ( ) ,
updated _at : new Date ( ) ,
uuid : uuidV1 ( )
} , data )
2017-11-23 16:26:53 +00:00
2020-12-13 14:36:07 +00:00
// Some data cleanups
2017-09-10 09:42:12 +00:00
2020-12-13 14:36:07 +00:00
// Limit display name length
udataLimited . display _name = udataLimited . display _name . substring ( 0 , 32 )
2017-09-10 09:42:12 +00:00
2020-12-13 14:36:07 +00:00
// Remove illegal characters from the username
udataLimited . username = udataLimited . username . replace ( /\W+/gi , '' )
2017-09-10 09:42:12 +00:00
2020-12-13 14:36:07 +00:00
// Limit user name length
udataLimited . username = udataLimited . username . substring ( 0 , 26 )
2017-08-24 18:36:40 +00:00
2020-12-13 14:36:07 +00:00
// Check if the username is already taken
if ( await User . get ( udataLimited . username ) != null || udataLimited . username . length < 4 ) {
udataLimited . username = udataLimited . username + Hash ( 4 )
}
2017-08-24 18:36:40 +00:00
2020-12-13 14:36:07 +00:00
// Check if the email given to us is already registered, if so,
// tell them to log in first.
if ( udataLimited . email && udataLimited . email !== '' ) {
const getByEmail = await User . get ( udataLimited . email )
if ( getByEmail ) {
throw new Error ( 'An user with this email address is already registered, but this external account is are not linked. If you wish to link the account, please log in first.' )
2017-08-24 18:36:40 +00:00
}
2020-12-13 14:36:07 +00:00
}
2017-08-24 18:36:40 +00:00
2020-12-13 14:36:07 +00:00
// Create a new user based on the information we got from an external service
const newUser = await models . User . query ( ) . insert ( udataLimited )
await Common . new ( service , identifier , newUser )
2017-08-25 16:42:30 +00:00
2020-12-13 14:36:07 +00:00
return newUser
}
2017-11-23 16:18:26 +00:00
2020-12-13 14:36:07 +00:00
// Remove an `external` object (thus unlinking from a service)
static async remove ( user , service ) {
user = await User . ensureObject ( user , [ 'password' ] )
const userExterns = await models . External . query ( ) . orderBy ( 'created_at' , 'asc' ) . where ( 'user_id' , user . id )
2017-11-23 16:18:26 +00:00
2020-12-13 14:36:07 +00:00
if ( ! userExterns . length ) {
return false
}
2017-11-23 16:18:26 +00:00
2020-12-13 14:36:07 +00:00
// Do not remove the service the user signed up with
if ( userExterns [ 0 ] && ( user . password === '' || user . password === null ) && userExterns [ 0 ] . service === service ) {
return false
}
2017-08-25 16:42:30 +00:00
2020-12-13 14:36:07 +00:00
return models . External . query ( ) . delete ( ) . where ( 'user_id' , user . id ) . andWhere ( 'service' , service )
}
2017-11-23 16:18:26 +00:00
2020-12-13 14:36:07 +00:00
// Common code for all auth callbacks
static async callback ( identifier , uid , user , ipAddress , remoteData , avatarFunc ) {
const exists = await Common . getExternal ( identifier , uid )
2017-11-23 16:18:26 +00:00
2020-12-13 14:36:07 +00:00
if ( user ) {
// Get bans for user
const bans = await Common . getBan ( user )
if ( bans . length ) return { banned : bans , ip : false }
2017-11-23 16:18:26 +00:00
2020-12-13 14:36:07 +00:00
if ( exists ) return { error : null , user : user }
2017-11-23 16:18:26 +00:00
2020-12-13 14:36:07 +00:00
await Common . new ( identifier , uid , user )
return { error : null , user : user }
2017-08-03 12:57:17 +00:00
}
2017-11-23 16:18:26 +00:00
2020-12-13 14:36:07 +00:00
// Callback succeeded with user id and the external table exists, we log in the user
if ( exists ) {
// Get bans for user
const bans = await Common . getBan ( exists . user )
2017-08-03 12:57:17 +00:00
2020-12-13 14:36:07 +00:00
if ( bans . length ) return { banned : bans , ip : false }
return { error : null , user : exists . user }
}
2017-08-03 12:57:17 +00:00
2020-12-13 14:36:07 +00:00
// Get bans for IP address
const bans = await Common . getBan ( null , ipAddress )
if ( bans . length ) return { banned : bans , ip : true }
2017-08-03 12:57:17 +00:00
2020-12-13 14:36:07 +00:00
// Run the function for avatar fetching
let avatar = null
if ( avatarFunc ) {
avatar = await avatarFunc ( remoteData )
}
2017-08-03 12:57:17 +00:00
2020-12-13 14:36:07 +00:00
// Assign the data
const newUData = Object . assign ( {
email : remoteData . email || '' ,
avatar _file : avatar ,
ip _address : ipAddress
} , remoteData )
// Remove unnecessary fields
for ( const i in newUData ) {
if ( userFields . indexOf ( i ) === - 1 ) {
delete newUData [ i ]
2017-08-03 12:57:17 +00:00
}
2020-12-13 14:36:07 +00:00
}
2017-08-03 12:57:17 +00:00
2020-12-13 14:36:07 +00:00
let newUser
try {
newUser = await Common . newUser ( identifier , uid , newUData )
} catch ( e ) {
return { error : e . message }
2017-08-03 12:57:17 +00:00
}
2017-11-23 16:18:26 +00:00
2020-12-13 14:36:07 +00:00
return { error : null , user : newUser }
}
}
export class Facebook {
static async getAvatar ( rawData ) {
let profilepic = null
if ( rawData . picture ) {
if ( rawData . picture . is _silhouette === false && rawData . picture . url ) {
const imgdata = await Image . downloadImage ( rawData . picture . url )
2017-11-23 16:18:26 +00:00
if ( imgdata && imgdata . fileName ) {
profilepic = imgdata . fileName
}
}
2020-12-13 14:36:07 +00:00
}
2017-11-23 16:18:26 +00:00
2020-12-13 14:36:07 +00:00
return profilepic
}
2017-08-24 16:23:03 +00:00
2020-12-13 14:36:07 +00:00
static async callback ( user , authResponse , ipAddress ) {
if ( ! authResponse ) {
return { error : 'No Authorization' }
}
2017-08-03 12:57:17 +00:00
2020-12-13 14:36:07 +00:00
const uid = authResponse . userID
if ( ! uid ) {
return { error : 'No Authorization' }
}
2017-08-03 12:57:17 +00:00
2020-12-13 14:36:07 +00:00
// Get facebook user information in order to create a new user or verify
let fbdata
const intel = {
access _token : authResponse . accessToken ,
fields : 'name,email,picture,short_name'
}
2017-08-03 12:57:17 +00:00
2020-12-13 14:36:07 +00:00
try {
fbdata = await httpGET ( 'https://graph.facebook.com/v2.10/' + uid + '?' + qs . stringify ( intel ) )
fbdata = JSON . parse ( fbdata )
} catch ( e ) {
return { error : 'Could not get user information' , errorObject : e }
}
2017-08-03 12:57:17 +00:00
2020-12-13 14:36:07 +00:00
if ( fbdata . error ) {
return { error : fbdata . error . message }
}
2017-08-03 12:57:17 +00:00
2020-12-13 14:36:07 +00:00
const cleanedData = Object . assign ( fbdata , {
username : fbdata . short _name || 'FB' + Hash ( 4 ) ,
display _name : fbdata . name ,
email : fbdata . email || ''
} )
2017-08-03 12:57:17 +00:00
2020-12-13 14:36:07 +00:00
return Common . callback ( 'facebook' , uid , user , ipAddress , cleanedData , Facebook . getAvatar )
}
}
2017-08-03 12:57:17 +00:00
2020-12-13 14:36:07 +00:00
export class Twitter {
static async getAvatar ( rawData ) {
let profilepic = null
2017-08-27 12:41:44 +00:00
2020-12-13 14:36:07 +00:00
if ( rawData . profile _image _url _https ) {
const imgdata = await Image . downloadImage ( rawData . profile _image _url _https )
if ( imgdata && imgdata . fileName ) {
profilepic = imgdata . fileName
2017-08-03 12:57:17 +00:00
}
2020-12-13 14:36:07 +00:00
}
2017-08-03 12:57:17 +00:00
2020-12-13 14:36:07 +00:00
return profilepic
}
2017-10-13 16:18:17 +00:00
2020-12-13 14:36:07 +00:00
static oauthApp ( ) {
if ( ! twitterApp ) {
const redirectUri = config . server . domain + '/api/external/twitter/callback'
twitterApp = new oauth . PromiseOAuth (
'https://api.twitter.com/oauth/request_token' ,
'https://api.twitter.com/oauth/access_token' ,
config . external . twitter . api ,
config . external . twitter . api _secret ,
'1.0A' ,
redirectUri ,
'HMAC-SHA1'
)
}
}
2017-10-13 16:18:17 +00:00
2020-12-13 14:36:07 +00:00
static async getRequestToken ( ) {
if ( ! twitterApp ) Twitter . oauthApp ( )
let tokens
2017-10-13 16:18:17 +00:00
2020-12-13 14:36:07 +00:00
try {
tokens = await twitterApp . getOAuthRequestToken ( )
} catch ( e ) {
console . error ( e )
return { error : 'No tokens returned' }
}
2017-10-13 16:18:17 +00:00
2020-12-13 14:36:07 +00:00
if ( tokens [ 2 ] . oauth _callback _confirmed !== 'true' ) return { error : 'No tokens returned.' }
2017-10-13 16:18:17 +00:00
2020-12-13 14:36:07 +00:00
return { error : null , token : tokens [ 0 ] , token _secret : tokens [ 1 ] }
}
2017-10-13 16:18:17 +00:00
2020-12-13 14:36:07 +00:00
static async getAccessTokens ( token , secret , verifier ) {
if ( ! twitterApp ) Twitter . oauthApp ( )
let tokens
2017-10-13 16:18:17 +00:00
2020-12-13 14:36:07 +00:00
try {
tokens = await twitterApp . getOAuthAccessToken ( token , secret , verifier )
} catch ( e ) {
console . error ( e )
return { error : 'No tokens returned' }
2017-10-13 16:18:17 +00:00
}
2017-11-23 16:18:26 +00:00
2020-12-13 14:36:07 +00:00
if ( ! tokens || ! tokens . length ) return { error : 'No tokens returned' }
2017-08-03 12:57:17 +00:00
2020-12-13 14:36:07 +00:00
return { error : null , access _token : tokens [ 0 ] , access _token _secret : tokens [ 1 ] }
}
2017-08-03 12:57:17 +00:00
2020-12-13 14:36:07 +00:00
static async callback ( user , accessTokens , ipAddress ) {
if ( ! twitterApp ) Twitter . oauthApp ( )
let twdata
try {
const resp = await twitterApp . get ( 'https://api.twitter.com/1.1/account/verify_credentials.json?include_email=true' ,
accessTokens . access _token , accessTokens . access _token _secret )
twdata = JSON . parse ( resp [ 0 ] )
} catch ( e ) {
console . error ( e )
return { error : 'Failed to verify user credentials.' }
}
2017-08-03 12:57:17 +00:00
2020-12-13 14:36:07 +00:00
const uid = twdata . id _str
2017-08-03 12:57:17 +00:00
2020-12-13 14:36:07 +00:00
const cleanedData = Object . assign ( twdata , {
username : twdata . screen _name ,
display _name : twdata . name ,
email : twdata . email || ''
} )
return Common . callback ( 'twitter' , uid , user , ipAddress , cleanedData , Twitter . getAvatar )
}
}
export class Google {
static async getAvatar ( rawData ) {
let profilepic = null
if ( rawData . image ) {
const imgdata = await Image . downloadImage ( rawData . image )
if ( imgdata && imgdata . fileName ) {
profilepic = imgdata . fileName
2017-08-03 12:57:17 +00:00
}
2020-12-13 14:36:07 +00:00
}
return profilepic
}
static async callback ( user , data , ipAddress ) {
let uid
try {
const test = await httpGET ( 'https://www.googleapis.com/oauth2/v3/tokeninfo?id_token=' + data . id _token )
if ( ! test ) throw new Error ( 'No response!' )
const jsondata = JSON . parse ( test )
if ( ! jsondata || ! jsondata . email || ! jsondata . name ) throw new Error ( 'Please allow Basic Profile and Email.' )
if ( jsondata . email !== data . email || jsondata . name !== data . name ) throw new Error ( 'Conflicting data. Please try again!' )
if ( new Date ( parseInt ( jsondata . exp ) * 1000 ) < Date . now ( ) ) throw new Error ( 'Expired token! Please try again!' )
uid = jsondata . sub
} catch ( e ) {
return { error : e . message }
}
2017-08-03 12:57:17 +00:00
2020-12-13 14:36:07 +00:00
const cleanedData = Object . assign ( data , {
username : data . name ,
display _name : data . name ,
email : data . email || ''
} )
2017-08-03 12:57:17 +00:00
2020-12-13 14:36:07 +00:00
return Common . callback ( 'google' , uid , user , ipAddress , cleanedData , Google . getAvatar )
}
}
2017-08-03 12:57:17 +00:00
2020-12-13 14:36:07 +00:00
export class Discord {
static async getAvatar ( rawData ) {
let profilepic = null
const aviSnowflake = rawData . avatar
if ( aviSnowflake ) {
2017-08-03 12:57:17 +00:00
try {
2020-12-13 14:36:07 +00:00
const avpt = await Image . downloadImage ( 'https://cdn.discordapp.com/avatars/' + rawData . id + '/' + aviSnowflake + '.png' )
if ( avpt && avpt . fileName ) {
profilepic = avpt . fileName
}
2017-08-03 12:57:17 +00:00
} catch ( e ) {
2020-12-13 14:36:07 +00:00
profilepic = null
2017-08-03 12:57:17 +00:00
}
2020-12-13 14:36:07 +00:00
}
2017-08-03 12:57:17 +00:00
2020-12-13 14:36:07 +00:00
return profilepic
}
2017-08-03 12:57:17 +00:00
2020-12-13 14:36:07 +00:00
static oauth2App ( ) {
if ( discordApp ) return
discordApp = new oauth . PromiseOAuth2 (
config . external . discord . api ,
config . external . discord . api _secret ,
'https://discordapp.com/api/' ,
'oauth2/authorize' ,
'oauth2/token'
)
discordApp . useAuthorizationHeaderforGET ( true )
}
2017-09-09 11:15:11 +00:00
2020-12-13 14:36:07 +00:00
static getAuthorizeURL ( req ) {
if ( ! discordApp ) Discord . oauth2App ( )
const state = Common . stateGenerator ( req )
const redirectUri = config . server . domain + '/api/external/discord/callback'
const params = {
client _id : config . external . discord . api ,
redirect _uri : redirectUri ,
scope : 'identify email' ,
response _type : 'code' ,
state : state
2017-08-03 12:57:17 +00:00
}
2020-12-13 14:36:07 +00:00
const url = discordApp . getAuthorizeUrl ( params )
return { error : null , state : state , url : url }
2017-08-03 12:57:17 +00:00
}
2020-12-13 14:36:07 +00:00
static async getAccessToken ( code ) {
if ( ! discordApp ) Discord . oauth2App ( )
const redirectUri = config . server . domain + '/api/external/discord/callback'
let tokens
try {
tokens = await discordApp . getOAuthAccessToken ( code , { grant _type : 'authorization_code' , redirect _uri : redirectUri } )
} catch ( e ) {
console . error ( e )
return { error : 'No Authorization' }
}
if ( ! tokens . length ) return { error : 'No Tokens' }
tokens = tokens [ 2 ]
return { error : null , accessToken : tokens . access _token }
}
static async callback ( user , accessToken , ipAddress ) {
if ( ! discordApp ) Discord . oauth2App ( )
let ddata
try {
const resp = await discordApp . get ( 'https://discordapp.com/api/users/@me' , accessToken )
ddata = JSON . parse ( resp )
} catch ( e ) {
console . error ( e )
return { error : 'Could not get user information' }
}
const uid = ddata . id
// Create a new user
const cleanedData = Object . assign ( ddata , {
display _name : ddata . username ,
email : ddata . email || ''
} )
return Common . callback ( 'discord' , uid , user , ipAddress , cleanedData , Discord . getAvatar )
}
}