This repository has been archived on 2022-11-26. You can view files and clone it, but cannot push or open issues or pull requests.
IcyNet.eu/server/api/external.js

449 lines
13 KiB
JavaScript

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)
}
}