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