import qs from 'querystring' import oauth from 'oauth-libre' import { v1 as uuidV1 } from 'uuid' import crypto from 'crypto' import config from '../../scripts/load-config' import { httpGET } from '../../scripts/http' import * as models from './models' import Image from './image' import { User, Hash } from './index' const userFields = ['username', 'email', 'avatar_file', 'display_name', 'ip_address'] let twitterApp let discordApp 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 if (extr.user_id !== null) { const user = await User.get(extr.user_id) if (user) { extr.user = user } } return extr } // Get user ban status static async getBan (user, ipAddress) { const banList = await User.getBanStatus(ipAddress || user.id, ipAddress != null) return banList } // 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() } await models.External.query().insert(data) return true } // 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) // 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 User.get(udataLimited.username) != null || udataLimited.username.length < 4) { udataLimited.username = udataLimited.username + 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 !== '') { 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.') } } // 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) return newUser } // 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) 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 static async callback (identifier, uid, user, ipAddress, remoteData, avatarFunc) { const exists = await Common.getExternal(identifier, uid) if (user) { // Get bans for user const bans = await Common.getBan(user) if (bans.length) return { banned: bans, ip: false } if (exists) return { error: null, user: user } await 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 const bans = await Common.getBan(exists.user) if (bans.length) return { banned: bans, ip: false } return { error: null, user: exists.user } } // Get bans for IP address const bans = await 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 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] } } let newUser try { newUser = await Common.newUser(identifier, uid, newUData) } catch (e) { return { error: e.message } } 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) if (imgdata && imgdata.fileName) { profilepic = imgdata.fileName } } } return profilepic } static async callback (user, authResponse, ipAddress) { if (!authResponse) { return { error: 'No Authorization' } } const uid = authResponse.userID if (!uid) { return { error: 'No Authorization' } } // 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' } 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 } } if (fbdata.error) { return { error: fbdata.error.message } } const cleanedData = Object.assign(fbdata, { username: fbdata.short_name || 'FB' + Hash(4), display_name: fbdata.name, email: fbdata.email || '' }) return Common.callback('facebook', uid, user, ipAddress, cleanedData, Facebook.getAvatar) } } export class Twitter { static async getAvatar (rawData) { let profilepic = null if (rawData.profile_image_url_https) { const imgdata = await Image.downloadImage(rawData.profile_image_url_https) if (imgdata && imgdata.fileName) { profilepic = imgdata.fileName } } return profilepic } 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' ) } } static async getRequestToken () { if (!twitterApp) 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] } } static async getAccessTokens (token, secret, verifier) { if (!twitterApp) 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] } } 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.' } } const uid = twdata.id_str 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 } } 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 } } const cleanedData = Object.assign(data, { username: data.name, display_name: data.name, email: data.email || '' }) return Common.callback('google', uid, user, ipAddress, cleanedData, Google.getAvatar) } } export class Discord { static async getAvatar (rawData) { let profilepic = null const aviSnowflake = rawData.avatar if (aviSnowflake) { try { const 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 } 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) } 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 } const url = discordApp.getAuthorizeUrl(params) return { error: null, state: state, url: url } } 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) } }