Change external login system so it uses common funtions, reducing repeated code

This commit is contained in:
Evert Prants 2017-11-23 18:18:26 +02:00
parent dd32345453
commit 8cb48abb39
Signed by: evert
GPG Key ID: 1688DA83D222D0B5
4 changed files with 192 additions and 267 deletions

View File

@ -1,20 +1,26 @@
import config from '../../scripts/load-config'
import http from '../../scripts/http'
import models from './models'
import image from './image'
import UAPI from './index'
import qs from 'querystring'
import oauth from 'oauth-libre'
import path from 'path'
import url from 'url'
import uuidV1 from 'uuid/v1'
import crypto from 'crypto'
const imgdir = path.join(__dirname, '../../', 'usercontent', 'images')
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
@ -30,10 +36,12 @@ const API = {
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,
@ -45,6 +53,7 @@ const API = {
await await models.External.query().insert(data)
return true
},
// Create a new user
newUser: async (service, identifier, data) => {
let udataLimited = Object.assign({
activated: 1,
@ -74,6 +83,7 @@ const API = {
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)
@ -89,30 +99,81 @@ const API = {
return models.External.query().delete().where('user_id', user.id).andWhere('service', service)
},
saveAvatar: async (avatarUrl) => {
if (!avatarUrl) return null
let imageName = 'download-' + UAPI.Hash(12)
let uridata = url.parse(avatarUrl)
let pathdata = path.parse(uridata.path)
// Common code for all auth callbacks
callback: async (identifier, uid, user, ipAddress, remoteData, avatarFunc) => {
let exists = await API.Common.getExternal(identifier, uid)
imageName += pathdata.ext || '.png'
if (user) {
// Get bans for user
let bans = await API.Common.getBan(user)
if (bans.length) return { banned: bans, ip: false }
try {
await http.Download(avatarUrl, path.join(imgdir, imageName))
} catch (e) {
return null
if (exists) return {error: null, user: user}
await API.Common.new(identifier, uid, user)
return {error: null, user: user}
}
return {fileName: imageName}
// 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 = await API.Common.newUser(identifier, uid, newUData)
if (!newUser) return {error: 'Failed to create user.'}
return {error: null, user: newUser}
}
},
Facebook: {
callback: async (user, data) => {
if (!data.authResponse || data.status !== 'connected') {
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 = data.authResponse.userID
let uid = authResponse.userID
if (!uid) {
return {error: 'No Authorization'}
}
@ -120,7 +181,7 @@ const API = {
// Get facebook user information in order to create a new user or verify
let fbdata
let intel = {
access_token: data.authResponse.accessToken,
access_token: authResponse.accessToken,
fields: 'name,email,picture,short_name'
}
@ -135,57 +196,28 @@ const API = {
return {error: fbdata.error.message}
}
let exists = await API.Common.getExternal('fb', uid)
let cleanedData = Object.assign(fbdata, {
username: fbdata.short_name || 'FB' + UAPI.Hash(4),
display_name: fbdata.name,
email: fbdata.email || ''
})
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('fb', uid, user)
return {error: null, user: user}
return API.Common.callback('facebook', uid, user, ipAddress, cleanedData, API.Facebook.getAvatar)
}
// 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, data.ip_address)
if (bans.length) return { banned: bans, ip: true }
// Determine profile picture
},
Twitter: {
getAvatar: async function (rawData) {
let profilepic = null
if (fbdata.picture) {
if (fbdata.picture.is_silhouette === false && fbdata.picture.url) {
let imgdata = await API.Common.saveAvatar(fbdata.picture.url)
if (rawData.profile_image_url_https) {
let imgdata = await image.downloadImage(rawData.profile_image_url_https)
if (imgdata && imgdata.fileName) {
profilepic = imgdata.fileName
}
}
}
let newUData = {
username: fbdata.short_name || 'FB' + UAPI.Hash(4),
display_name: fbdata.name,
email: fbdata.email || '',
avatar_file: profilepic,
ip_address: data.ip_address
}
let newUser = await API.Common.newUser('fb', uid, newUData)
if (!newUser) return {error: 'Failed to create user.'}
return {error: null, user: newUser}
}
return profilepic
},
Twitter: {
oauthApp: function () {
if (!twitterApp) {
let redirectUri = config.server.domain + '/api/external/twitter/callback'
@ -243,56 +275,28 @@ const API = {
}
let uid = twdata.id_str
let exists = await API.Common.getExternal('twitter', uid)
if (user) {
// Get bans for user
let bans = await API.Common.getBan(user)
if (bans.length) return { banned: bans, ip: false }
let cleanedData = Object.assign(twdata, {
username: twdata.screen_name,
display_name: twdata.name,
email: twdata.email || ''
})
if (exists) return {error: null, user: user}
await API.Common.new('twitter', uid, user)
return {error: null, user: user}
return API.Common.callback('twitter', uid, user, ipAddress, cleanedData, API.Twitter.getAvatar)
}
// 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
let bans = await API.Common.getBan(null, ipAddress)
if (bans.length) return { banned: bans, ip: true }
// Determine profile picture
},
Google: {
getAvatar: async (rawData) => {
let profilepic = null
if (twdata.profile_image_url_https) {
let imgdata = await API.Common.saveAvatar(twdata.profile_image_url_https)
if (rawData.image) {
let imgdata = await image.downloadImage(rawData.image)
if (imgdata && imgdata.fileName) {
profilepic = imgdata.fileName
}
}
// Create a new user
let newUData = {
username: twdata.screen_name,
display_name: twdata.name,
email: twdata.email || '',
avatar_file: profilepic,
ip_address: ipAddress
}
let newUser = await API.Common.newUser('twitter', uid, newUData)
if (!newUser) return {error: 'Failed to create user.'}
return {error: null, user: newUser}
}
return profilepic
},
Google: {
callback: async (user, data, ipAddress) => {
let uid
@ -312,56 +316,32 @@ const API = {
return {error: e.message}
}
let exists = await API.Common.getExternal('google', 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('google', 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
let bans = await API.Common.getBan(null, ipAddress)
if (bans.length) return { banned: bans, ip: true }
// Determine profile picture
let profilepic = null
if (data.image) {
let imgdata = await API.Common.saveAvatar(data.image)
if (imgdata && imgdata.fileName) {
profilepic = imgdata.fileName
}
}
// Create a new user
let newUData = {
let cleanedData = Object.assign(data, {
username: data.name.replace(/\W+/gi, ''),
display_name: data.name,
email: data.email || '',
avatar_file: profilepic,
ip_address: ipAddress
}
email: data.email || ''
})
let newUser = await API.Common.newUser('google', uid, newUData)
if (!newUser) return {error: 'Failed to create user.'}
return {error: null, user: newUser}
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(
@ -374,9 +354,9 @@ const API = {
discordApp.useAuthorizationHeaderforGET(true)
},
getAuthorizeURL: function () {
getAuthorizeURL: function (req) {
if (!discordApp) API.Discord.oauth2App()
let state = UAPI.Hash(6)
let state = API.Common.stateGenerator(req)
let redirectUri = config.server.domain + '/api/external/discord/callback'
const params = {
@ -421,58 +401,15 @@ const API = {
}
let uid = ddata.id
let exists = await API.Common.getExternal('discord', 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('discord', 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
let bans = await API.Common.getBan(null, ipAddress)
if (bans.length) return { banned: bans, ip: true }
// Download profile picture
let profilepic = null
let aviSnowflake = ddata.avatar
if (aviSnowflake) {
try {
let avpt = await API.Common.saveAvatar('https://cdn.discordapp.com/avatars/' + ddata.id + '/' + aviSnowflake + '.png')
if (avpt && avpt.fileName) {
profilepic = avpt.fileName
}
} catch (e) {
profilepic = null
}
}
// Create a new user
let newUData = {
let cleanedData = Object.assign(ddata, {
username: ddata.username.replace(/\W+/gi, '_'),
display_name: ddata.username,
email: ddata.email || '',
avatar_file: profilepic,
ip_address: ipAddress
}
email: ddata.email || ''
})
let newUser = await API.Common.newUser('discord', uid, newUData)
if (!newUser) return {error: 'Failed to create user.'}
return {error: null, user: newUser}
return API.Common.callback('discord', uid, user, ipAddress, cleanedData, API.Discord.getAvatar)
}
}
}

View File

@ -1,8 +1,11 @@
import gm from 'gm'
import url from 'url'
import path from 'path'
import crypto from 'crypto'
import Promise from 'bluebird'
import http from '../../scripts/http'
const fs = Promise.promisifyAll(require('fs'))
const uploads = path.join(__dirname, '../../', 'usercontent')
@ -14,6 +17,10 @@ const imageTypes = {
'image/jpeg': '.jpeg'
}
function imageUniquifier () {
return crypto.randomBytes(12).toString('hex')
}
function decodeBase64Image (dataString) {
let matches = dataString.match(/^data:([A-Za-z-+/]+);base64,(.+)$/)
let response = {}
@ -53,7 +60,7 @@ async function imageBase64 (baseObj) {
if (!imgData) return null
if (!imageTypes[imgData.type]) return null
let imageName = 'base64-' + crypto.randomBytes(12).toString('hex')
let imageName = 'base64-' + imageUniquifier()
let ext = imageTypes[imgData.type] || '.png'
imageName += ext
@ -70,6 +77,25 @@ async function imageBase64 (baseObj) {
return {file: fpath}
}
async function downloadImage (imgUrl, designation) {
if (!imgUrl) return null
if (!designation) designation = 'download'
let imageName = designation + '-' + imageUniquifier()
let uridata = url.parse(imgUrl)
let pathdata = path.parse(uridata.path)
imageName += pathdata.ext || '.png'
try {
await http.Download(imgUrl, path.join(images, imageName))
} catch (e) {
return null
}
return {fileName: imageName}
}
async function uploadImage (identifier, fields, files) {
if (!files.image) return {error: 'No image file'}
@ -142,6 +168,7 @@ async function uploadImage (identifier, fields, files) {
}
module.exports = {
downloadImage: downloadImage,
uploadImage: uploadImage,
imageBase64: imageBase64,
types: imageTypes

View File

@ -79,16 +79,34 @@ function JsonData (req, res, error, redirect = '/') {
res.jsonp({error: error, redirect: redirect})
}
// Common middleware for all external account unlinks
function removeAuthMiddleware (identifier) {
return wrap(async (req, res) => {
if (!req.session.user) return res.redirect('/login')
let done = await APIExtern.Common.remove(req.session.user, identifier)
if (!done) {
req.flash('message', {error: true, text: 'Unable to unlink social media account'})
}
res.redirect('/user/manage')
})
}
/** FACEBOOK LOGIN
* Ajax POST only <in-page javascript handeled>
* No tokens saved in configs, everything works out-of-the-box
*/
router.post('/external/facebook/callback', wrap(async (req, res, next) => {
if (!config.facebook || !config.facebook.client) return next()
let sane = objectAssembler(req.body)
sane.ip_address = req.realIP
let response = await APIExtern.Facebook.callback(req.session.user, sane)
// Fix up the retarded object Facebook sends us
let sane = objectAssembler(req.body)
if (!sane || !sane.authResponse) {
return next()
}
let response = await APIExtern.Facebook.callback(req.session.user, sane.authResponse, req.realIP)
if (response.banned) {
return JsonData(req, res, 'You are banned.')
@ -107,16 +125,7 @@ router.post('/external/facebook/callback', wrap(async (req, res, next) => {
JsonData(req, res, null, '/login')
}))
router.get('/external/facebook/remove', wrap(async (req, res) => {
if (!req.session.user) return res.redirect('/login')
let done = await APIExtern.Common.remove(req.session.user, 'fb')
if (!done) {
req.flash('message', {error: true, text: 'Unable to unlink social media account'})
}
res.redirect('/user/manage')
}))
router.get('/external/facebook/remove', removeAuthMiddleware('facebook'))
/** TWITTER LOGIN
* OAuth1.0a flows
@ -172,17 +181,7 @@ router.get('/external/twitter/callback', wrap(async (req, res) => {
res.render('redirect', {url: uri})
}))
router.get('/external/twitter/remove', wrap(async (req, res) => {
if (!req.session.user) return res.redirect('/login')
let done = await APIExtern.Common.remove(req.session.user, 'twitter')
if (!done) {
req.flash('message', {error: true, text: 'Unable to unlink social media account'})
}
res.redirect('/user/manage')
}))
router.get('/external/twitter/remove', removeAuthMiddleware('twitter'))
/** DISCORD LOGIN
* OAuth2 flows
@ -191,22 +190,16 @@ router.get('/external/twitter/remove', wrap(async (req, res) => {
router.get('/external/discord/login', wrap(async (req, res) => {
if (!config.discord || !config.discord.api) return res.redirect('/')
let infos = APIExtern.Discord.getAuthorizeURL()
req.session.discord_auth = {
state: infos.state
}
let infos = APIExtern.Discord.getAuthorizeURL(req)
res.redirect(infos.url)
}))
router.get('/external/discord/callback', wrap(async (req, res) => {
if (!config.discord || !config.discord.api) return res.redirect('/login')
if (!req.session.discord_auth) return res.redirect('/login')
let code = req.query.code
let state = req.query.state
let da = req.session.discord_auth
let uri = '/login'
if (!code) {
@ -214,7 +207,7 @@ router.get('/external/discord/callback', wrap(async (req, res) => {
return res.redirect(uri)
}
if (!state || state !== da.state) {
if (!state || state !== APIExtern.Common.stateGenerator(req)) {
req.flash('message', {error: true, text: 'Request got intercepted, try again.'})
return res.redirect(uri)
}
@ -245,29 +238,7 @@ router.get('/external/discord/callback', wrap(async (req, res) => {
res.render('redirect', {url: uri})
}))
router.get('/external/discord/remove', wrap(async (req, res) => {
if (!req.session.user) return res.redirect('/login')
let done = await APIExtern.Common.remove(req.session.user, 'discord')
if (!done) {
req.flash('message', {error: true, text: 'Unable to unlink social media account'})
}
res.redirect('/user/manage')
}))
router.get('/external/discord/remove', wrap(async (req, res) => {
if (!req.session.user) return res.redirect('/login')
let done = await APIExtern.Common.remove(req.session.user, 'discord')
if (!done) {
req.flash('message', {error: true, text: 'Unable to unlink social media account'})
}
res.redirect('/user/manage')
}))
router.get('/external/discord/remove', removeAuthMiddleware('discord'))
/** GOOGLE LOGIN
* Google Token Verification
@ -303,17 +274,7 @@ router.post('/external/google/callback', wrap(async (req, res) => {
JsonData(req, res, null, '/login')
}))
router.get('/external/google/remove', wrap(async (req, res) => {
if (!req.session.user) return res.redirect('/login')
let done = await APIExtern.Common.remove(req.session.user, 'google')
if (!done) {
req.flash('message', {error: true, text: 'Unable to unlink social media account'})
}
res.redirect('/user/manage')
}))
router.get('/external/google/remove', removeAuthMiddleware('google'))
/* ========
* NEWS

View File

@ -205,9 +205,9 @@ router.get('/user/manage', ensureLogin, wrap(async (req, res) => {
}
if (config.facebook && config.facebook.client) {
if (!socialStatus.enabled.fb) {
if (!socialStatus.enabled.facebook) {
res.locals.facebook_auth = config.facebook.client
} else if (socialStatus.source !== 'fb') {
} else if (socialStatus.source !== 'facebook') {
res.locals.facebook_auth = false
}
}