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) => { 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.twitter.api, config.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.discord.api, config.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.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