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

433 lines
13 KiB
JavaScript
Raw Normal View History

import qs from 'querystring'
import oauth from 'oauth-libre'
2020-05-28 18:30:21 +00:00
import { v1 as uuidV1 } from 'uuid'
import crypto from 'crypto'
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'
import UAPI from './index'
const userFields = ['username', 'email', 'avatar_file', 'display_name', 'ip_address']
2017-09-09 11:15:11 +00:00
let twitterApp
let discordApp
const API = {
Common: {
// 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
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) {
2020-05-28 18:30:21 +00:00
const user = await UAPI.User.get(extr.user_id)
if (user) {
extr.user = user
}
}
return extr
},
// 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
},
// Create a new `external` instance for a user
new: async (service, identifier, user) => {
2020-05-28 18:30:21 +00:00
const 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
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(),
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
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,
// 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) {
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
},
// Remove an `external` object (thus unlinking from a service)
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)
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
},
// 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
if (user) {
// Get bans for user
2020-05-28 18:30:21 +00:00
const bans = await API.Common.getBan(user)
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 }
await API.Common.new(identifier, uid, user)
2020-05-28 18:30:21 +00:00
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
2020-05-28 18:30:21 +00:00
const bans = await API.Common.getBan(exists.user)
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
}
// Get bans for IP address
2020-05-28 18:30:21 +00:00
const 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
2020-05-28 18:30:21 +00:00
const newUData = Object.assign({
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) {
if (userFields.indexOf(i) === -1) {
delete newUData[i]
}
}
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 }
}
2020-05-28 18:30:21 +00:00
return { error: null, user: newUser }
}
},
Facebook: {
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)
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' }
}
2020-05-28 18:30:21 +00:00
const uid = authResponse.userID
if (!uid) {
2020-05-28 18:30:21 +00:00
return { error: 'No Authorization' }
}
// 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 = {
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) {
2020-05-28 18:30:21 +00:00
return { error: 'Could not get user information', errorObject: e }
}
if (fbdata.error) {
2020-05-28 18:30:21 +00:00
return { error: fbdata.error.message }
}
2020-05-28 18:30:21 +00:00
const 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) {
2020-05-28 18:30:21 +00:00
const imgdata = await Image.downloadImage(rawData.profile_image_url_https)
if (imgdata && imgdata.fileName) {
profilepic = imgdata.fileName
}
}
return profilepic
},
oauthApp: function () {
if (!twitterApp) {
2020-05-28 18:30:21 +00:00
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',
2019-08-08 12:33:58 +00:00
config.external.twitter.api,
config.external.twitter.api_secret,
'1.0A',
redirectUri,
'HMAC-SHA1'
)
}
},
getRequestToken: async function () {
if (!twitterApp) API.Twitter.oauthApp()
let tokens
2017-08-24 16:23:03 +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' }
}
2020-05-28 18:30:21 +00:00
if (tokens[2].oauth_callback_confirmed !== 'true') return { error: 'No tokens returned.' }
2020-05-28 18:30:21 +00:00
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)
2020-05-28 18:30:21 +00:00
return { error: 'No tokens returned' }
}
2020-05-28 18:30:21 +00:00
if (!tokens || !tokens.length) return { error: 'No tokens returned' }
2020-05-28 18:30:21 +00:00
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 {
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)
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.' }
}
2020-05-28 18:30:21 +00:00
const uid = twdata.id_str
2020-05-28 18:30:21 +00:00
const cleanedData = Object.assign(twdata, {
username: twdata.screen_name,
display_name: twdata.name,
email: twdata.email || ''
})
2017-08-27 12:41:44 +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
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
}
}
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,
email: data.email || ''
})
2017-10-13 16:18:17 +00:00
return API.Common.callback('google', uid, user, ipAddress, cleanedData, API.Google.getAvatar)
2017-10-13 16:18:17 +00:00
}
},
Discord: {
getAvatar: async (rawData) => {
let profilepic = null
2020-05-28 18:30:21 +00:00
const aviSnowflake = rawData.avatar
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')
if (avpt && avpt.fileName) {
profilepic = avpt.fileName
}
} catch (e) {
profilepic = null
}
}
return profilepic
},
2017-08-24 16:23:03 +00:00
oauth2App: function () {
if (discordApp) return
discordApp = new oauth.PromiseOAuth2(
2019-08-08 12:33:58 +00:00
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()
2020-05-28 18:30:21 +00:00
const state = API.Common.stateGenerator(req)
const redirectUri = config.server.domain + '/api/external/discord/callback'
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
}
2020-05-28 18:30:21 +00:00
const url = discordApp.getAuthorizeUrl(params)
2020-05-28 18:30:21 +00:00
return { error: null, state: state, url: url }
},
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'
let tokens
try {
2020-05-28 18:30:21 +00:00
tokens = await discordApp.getOAuthAccessToken(code, { grant_type: 'authorization_code', redirect_uri: redirectUri })
} catch (e) {
console.error(e)
2020-05-28 18:30:21 +00:00
return { error: 'No Authorization' }
}
2020-05-28 18:30:21 +00:00
if (!tokens.length) return { error: 'No Tokens' }
tokens = tokens[2]
2020-05-28 18:30:21 +00:00
return { error: null, accessToken: tokens.access_token }
},
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)
ddata = JSON.parse(resp)
} catch (e) {
console.error(e)
2020-05-28 18:30:21 +00:00
return { error: 'Could not get user information' }
}
2020-05-28 18:30:21 +00:00
const uid = ddata.id
// Create a new user
2020-05-28 18:30:21 +00:00
const cleanedData = Object.assign(ddata, {
display_name: ddata.username,
email: ddata.email || ''
})
2017-09-09 11:15:11 +00:00
return API.Common.callback('discord', uid, user, ipAddress, cleanedData, API.Discord.getAvatar)
}
}
}
module.exports = API