433 lines
13 KiB
JavaScript
433 lines
13 KiB
JavaScript
import qs from 'querystring'
|
|
import oauth from 'oauth-libre'
|
|
import uuidV1 from 'uuid/v1'
|
|
import crypto from 'crypto'
|
|
|
|
import config from '../../scripts/load-config'
|
|
import http from '../../scripts/http'
|
|
import models from './models'
|
|
import Image from './image'
|
|
import UAPI from './index'
|
|
|
|
const userFields = ['username', 'email', 'avatar_file', 'display_name', 'ip_address']
|
|
|
|
let twitterApp
|
|
let discordApp
|
|
|
|
const API = {
|
|
Common: {
|
|
// Generate a hash based on the current session
|
|
stateGenerator: (req) => {
|
|
let sessionCrypto = req.session.id + ':' + config.server.session_secret
|
|
return crypto.createHash('sha256').update(sessionCrypto).digest('hex')
|
|
},
|
|
// Find an user with an external ID
|
|
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) {
|
|
let user = await UAPI.User.get(extr.user_id)
|
|
if (user) {
|
|
extr.user = user
|
|
}
|
|
}
|
|
|
|
return extr
|
|
},
|
|
// Get user ban status
|
|
getBan: async (user, ipAddress) => {
|
|
let banList = await UAPI.User.getBanStatus(ipAddress || user.id, ipAddress != null)
|
|
return banList
|
|
},
|
|
// Create a new `external` instance for a user
|
|
new: async (service, identifier, user) => {
|
|
let data = {
|
|
user_id: user.id,
|
|
service: service,
|
|
identifier: identifier,
|
|
created_at: new Date()
|
|
}
|
|
|
|
await await models.External.query().insert(data)
|
|
return true
|
|
},
|
|
// Create a new user
|
|
newUser: async (service, identifier, data) => {
|
|
if (config.external.registrations !== true) throw new Error('Registrations from third-party websites are not allowed.')
|
|
let udataLimited = Object.assign({
|
|
activated: 1,
|
|
created_at: new Date(),
|
|
updated_at: new Date(),
|
|
uuid: uuidV1()
|
|
}, data)
|
|
|
|
// 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)
|
|
|
|
// Check if the username is already taken
|
|
if (await UAPI.User.get(udataLimited.username) != null || udataLimited.username.length < 4) {
|
|
udataLimited.username = udataLimited.username + UAPI.Hash(4)
|
|
}
|
|
|
|
// Check if the email given to us is already registered, if so,
|
|
// tell them to log in first.
|
|
if (udataLimited.email && udataLimited.email !== '') {
|
|
let getByEmail = await UAPI.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.')
|
|
}
|
|
}
|
|
|
|
// Create a new user based on the information we got from an external service
|
|
let newUser = await models.User.query().insert(udataLimited)
|
|
await API.Common.new(service, identifier, newUser)
|
|
|
|
return newUser
|
|
},
|
|
// Remove an `external` object (thus unlinking from a service)
|
|
remove: async (user, service) => {
|
|
user = await UAPI.User.ensureObject(user, ['password'])
|
|
let userExterns = await models.External.query().orderBy('created_at', 'asc').where('user_id', user.id)
|
|
|
|
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)
|
|
},
|
|
// Common code for all auth callbacks
|
|
callback: async (identifier, uid, user, ipAddress, remoteData, avatarFunc) => {
|
|
let exists = await API.Common.getExternal(identifier, uid)
|
|
|
|
if (user) {
|
|
// Get bans for user
|
|
let bans = await API.Common.getBan(user)
|
|
if (bans.length) return { banned: bans, ip: false }
|
|
|
|
if (exists) return {error: null, user: user}
|
|
|
|
await API.Common.new(identifier, uid, user)
|
|
return {error: null, user: user}
|
|
}
|
|
|
|
// Callback succeeded with user id and the external table exists, we log in the user
|
|
if (exists) {
|
|
// Get bans for user
|
|
let bans = await API.Common.getBan(exists.user)
|
|
|
|
if (bans.length) return { banned: bans, ip: false }
|
|
return {error: null, user: exists.user}
|
|
}
|
|
|
|
// Get bans for IP address
|
|
let bans = await API.Common.getBan(null, ipAddress)
|
|
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
|
|
let newUData = Object.assign({
|
|
email: remoteData.email || '',
|
|
avatar_file: avatar,
|
|
ip_address: ipAddress
|
|
}, remoteData)
|
|
|
|
// Remove unnecessary fields
|
|
for (let i in newUData) {
|
|
if (userFields.indexOf(i) === -1) {
|
|
delete newUData[i]
|
|
}
|
|
}
|
|
|
|
let newUser
|
|
try {
|
|
newUser = await API.Common.newUser(identifier, uid, newUData)
|
|
} catch (e) {
|
|
return {error: e.message}
|
|
}
|
|
|
|
return {error: null, user: newUser}
|
|
}
|
|
},
|
|
Facebook: {
|
|
getAvatar: async (rawData) => {
|
|
let profilepic = null
|
|
|
|
if (rawData.picture) {
|
|
if (rawData.picture.is_silhouette === false && rawData.picture.url) {
|
|
let imgdata = await Image.downloadImage(rawData.picture.url)
|
|
if (imgdata && imgdata.fileName) {
|
|
profilepic = imgdata.fileName
|
|
}
|
|
}
|
|
}
|
|
|
|
return profilepic
|
|
},
|
|
callback: async (user, authResponse, ipAddress) => {
|
|
if (!authResponse) {
|
|
return {error: 'No Authorization'}
|
|
}
|
|
|
|
let uid = authResponse.userID
|
|
if (!uid) {
|
|
return {error: 'No Authorization'}
|
|
}
|
|
|
|
// Get facebook user information in order to create a new user or verify
|
|
let fbdata
|
|
let intel = {
|
|
access_token: authResponse.accessToken,
|
|
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) {
|
|
return {error: 'Could not get user information', errorObject: e}
|
|
}
|
|
|
|
if (fbdata.error) {
|
|
return {error: fbdata.error.message}
|
|
}
|
|
|
|
let cleanedData = Object.assign(fbdata, {
|
|
username: fbdata.short_name || 'FB' + UAPI.Hash(4),
|
|
display_name: fbdata.name,
|
|
email: fbdata.email || ''
|
|
})
|
|
|
|
return API.Common.callback('facebook', uid, user, ipAddress, cleanedData, API.Facebook.getAvatar)
|
|
}
|
|
},
|
|
Twitter: {
|
|
getAvatar: async function (rawData) {
|
|
let profilepic = null
|
|
|
|
if (rawData.profile_image_url_https) {
|
|
let imgdata = await Image.downloadImage(rawData.profile_image_url_https)
|
|
if (imgdata && imgdata.fileName) {
|
|
profilepic = imgdata.fileName
|
|
}
|
|
}
|
|
|
|
return profilepic
|
|
},
|
|
oauthApp: function () {
|
|
if (!twitterApp) {
|
|
let 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'
|
|
)
|
|
}
|
|
},
|
|
getRequestToken: async function () {
|
|
if (!twitterApp) API.Twitter.oauthApp()
|
|
let tokens
|
|
|
|
try {
|
|
tokens = await twitterApp.getOAuthRequestToken()
|
|
} catch (e) {
|
|
console.error(e)
|
|
return {error: 'No tokens returned'}
|
|
}
|
|
|
|
if (tokens[2].oauth_callback_confirmed !== 'true') return {error: 'No tokens returned.'}
|
|
|
|
return {error: null, token: tokens[0], token_secret: tokens[1]}
|
|
},
|
|
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)
|
|
return {error: 'No tokens returned'}
|
|
}
|
|
|
|
if (!tokens || !tokens.length) return {error: 'No tokens returned'}
|
|
|
|
return {error: null, access_token: tokens[0], access_token_secret: tokens[1]}
|
|
},
|
|
callback: async function (user, accessTokens, ipAddress) {
|
|
if (!twitterApp) API.Twitter.oauthApp()
|
|
let twdata
|
|
try {
|
|
let 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.'}
|
|
}
|
|
|
|
let uid = twdata.id_str
|
|
|
|
let cleanedData = Object.assign(twdata, {
|
|
username: twdata.screen_name,
|
|
display_name: twdata.name,
|
|
email: twdata.email || ''
|
|
})
|
|
|
|
return API.Common.callback('twitter', uid, user, ipAddress, cleanedData, API.Twitter.getAvatar)
|
|
}
|
|
},
|
|
Google: {
|
|
getAvatar: async (rawData) => {
|
|
let profilepic = null
|
|
if (rawData.image) {
|
|
let imgdata = await Image.downloadImage(rawData.image)
|
|
if (imgdata && imgdata.fileName) {
|
|
profilepic = imgdata.fileName
|
|
}
|
|
}
|
|
|
|
return profilepic
|
|
},
|
|
callback: async (user, data, ipAddress) => {
|
|
let uid
|
|
|
|
try {
|
|
let test = await http.GET('https://www.googleapis.com/oauth2/v3/tokeninfo?id_token=' + data.id_token)
|
|
if (!test) throw new Error('No response!')
|
|
|
|
let 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}
|
|
}
|
|
|
|
let cleanedData = Object.assign(data, {
|
|
username: data.name,
|
|
display_name: data.name,
|
|
email: data.email || ''
|
|
})
|
|
|
|
return API.Common.callback('google', uid, user, ipAddress, cleanedData, API.Google.getAvatar)
|
|
}
|
|
},
|
|
Discord: {
|
|
getAvatar: async (rawData) => {
|
|
let profilepic = null
|
|
let aviSnowflake = rawData.avatar
|
|
if (aviSnowflake) {
|
|
try {
|
|
let avpt = await Image.downloadImage('https://cdn.discordapp.com/avatars/' + rawData.id + '/' + aviSnowflake + '.png')
|
|
if (avpt && avpt.fileName) {
|
|
profilepic = avpt.fileName
|
|
}
|
|
} catch (e) {
|
|
profilepic = null
|
|
}
|
|
}
|
|
|
|
return profilepic
|
|
},
|
|
oauth2App: function () {
|
|
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)
|
|
},
|
|
getAuthorizeURL: function (req) {
|
|
if (!discordApp) API.Discord.oauth2App()
|
|
let state = API.Common.stateGenerator(req)
|
|
let 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
|
|
}
|
|
|
|
let url = discordApp.getAuthorizeUrl(params)
|
|
|
|
return {error: null, state: state, url: url}
|
|
},
|
|
getAccessToken: async function (code) {
|
|
if (!discordApp) API.Discord.oauth2App()
|
|
|
|
let 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}
|
|
},
|
|
callback: async function (user, accessToken, ipAddress) {
|
|
if (!discordApp) API.Discord.oauth2App()
|
|
|
|
let ddata
|
|
try {
|
|
let 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'}
|
|
}
|
|
|
|
let uid = ddata.id
|
|
|
|
// Create a new user
|
|
let cleanedData = Object.assign(ddata, {
|
|
display_name: ddata.username,
|
|
email: ddata.email || ''
|
|
})
|
|
|
|
return API.Common.callback('discord', uid, user, ipAddress, cleanedData, API.Discord.getAvatar)
|
|
}
|
|
}
|
|
}
|
|
|
|
module.exports = API
|