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 config from '../../scripts/load-config'
import http from '../../scripts/http' import http from '../../scripts/http'
import models from './models' import models from './models'
import image from './image'
import UAPI from './index' import UAPI from './index'
import qs from 'querystring' import qs from 'querystring'
import oauth from 'oauth-libre' import oauth from 'oauth-libre'
import path from 'path'
import url from 'url'
import uuidV1 from 'uuid/v1' 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 twitterApp
let discordApp let discordApp
const API = { const API = {
Common: { 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) => { getExternal: async (service, identifier) => {
let extr = await models.External.query().where('service', service).andWhere('identifier', identifier) let extr = await models.External.query().where('service', service).andWhere('identifier', identifier)
if (!extr || !extr.length) return null if (!extr || !extr.length) return null
@ -30,10 +36,12 @@ const API = {
return extr return extr
}, },
// Get user ban status
getBan: async (user, ipAddress) => { getBan: async (user, ipAddress) => {
let banList = await UAPI.User.getBanStatus(ipAddress || user.id, ipAddress != null) let banList = await UAPI.User.getBanStatus(ipAddress || user.id, ipAddress != null)
return banList return banList
}, },
// Create a new `external` instance for a user
new: async (service, identifier, user) => { new: async (service, identifier, user) => {
let data = { let data = {
user_id: user.id, user_id: user.id,
@ -45,6 +53,7 @@ const API = {
await await models.External.query().insert(data) await await models.External.query().insert(data)
return true return true
}, },
// Create a new user
newUser: async (service, identifier, data) => { newUser: async (service, identifier, data) => {
let udataLimited = Object.assign({ let udataLimited = Object.assign({
activated: 1, activated: 1,
@ -74,6 +83,7 @@ const API = {
return newUser return newUser
}, },
// Remove an `external` object (thus unlinking from a service)
remove: async (user, service) => { remove: async (user, service) => {
user = await UAPI.User.ensureObject(user, ['password']) user = await UAPI.User.ensureObject(user, ['password'])
let userExterns = await models.External.query().orderBy('created_at', 'asc').where('user_id', user.id) 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) return models.External.query().delete().where('user_id', user.id).andWhere('service', service)
}, },
saveAvatar: async (avatarUrl) => { // Common code for all auth callbacks
if (!avatarUrl) return null callback: async (identifier, uid, user, ipAddress, remoteData, avatarFunc) => {
let imageName = 'download-' + UAPI.Hash(12) let exists = await API.Common.getExternal(identifier, uid)
let uridata = url.parse(avatarUrl)
let pathdata = path.parse(uridata.path)
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 { if (exists) return {error: null, user: user}
await http.Download(avatarUrl, path.join(imgdir, imageName))
} catch (e) { await API.Common.new(identifier, uid, user)
return null 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: { Facebook: {
callback: async (user, data) => { getAvatar: async (rawData) => {
if (!data.authResponse || data.status !== 'connected') { 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'} return {error: 'No Authorization'}
} }
let uid = data.authResponse.userID let uid = authResponse.userID
if (!uid) { if (!uid) {
return {error: 'No Authorization'} return {error: 'No Authorization'}
} }
@ -120,7 +181,7 @@ const API = {
// Get facebook user information in order to create a new user or verify // Get facebook user information in order to create a new user or verify
let fbdata let fbdata
let intel = { let intel = {
access_token: data.authResponse.accessToken, access_token: authResponse.accessToken,
fields: 'name,email,picture,short_name' fields: 'name,email,picture,short_name'
} }
@ -135,57 +196,28 @@ const API = {
return {error: fbdata.error.message} 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) { return API.Common.callback('facebook', uid, user, ipAddress, cleanedData, API.Facebook.getAvatar)
// 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}
} }
},
// Callback succeeded with user id and the external table exists, we log in the user Twitter: {
if (exists) { getAvatar: async function (rawData) {
// 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
let profilepic = null let profilepic = null
if (fbdata.picture) {
if (fbdata.picture.is_silhouette === false && fbdata.picture.url) { if (rawData.profile_image_url_https) {
let imgdata = await API.Common.saveAvatar(fbdata.picture.url) let imgdata = await image.downloadImage(rawData.profile_image_url_https)
if (imgdata && imgdata.fileName) { if (imgdata && imgdata.fileName) {
profilepic = imgdata.fileName profilepic = imgdata.fileName
} }
} }
}
let newUData = { return profilepic
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}
}
}, },
Twitter: {
oauthApp: function () { oauthApp: function () {
if (!twitterApp) { if (!twitterApp) {
let redirectUri = config.server.domain + '/api/external/twitter/callback' let redirectUri = config.server.domain + '/api/external/twitter/callback'
@ -243,56 +275,28 @@ const API = {
} }
let uid = twdata.id_str let uid = twdata.id_str
let exists = await API.Common.getExternal('twitter', uid)
if (user) { let cleanedData = Object.assign(twdata, {
// Get bans for user username: twdata.screen_name,
let bans = await API.Common.getBan(user) display_name: twdata.name,
if (bans.length) return { banned: bans, ip: false } email: twdata.email || ''
})
if (exists) return {error: null, user: user} return API.Common.callback('twitter', uid, user, ipAddress, cleanedData, API.Twitter.getAvatar)
await API.Common.new('twitter', uid, user)
return {error: null, user: user}
} }
},
// Callback succeeded with user id and the external table exists, we log in the user Google: {
if (exists) { getAvatar: async (rawData) => {
// 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 let profilepic = null
if (twdata.profile_image_url_https) { if (rawData.image) {
let imgdata = await API.Common.saveAvatar(twdata.profile_image_url_https) let imgdata = await image.downloadImage(rawData.image)
if (imgdata && imgdata.fileName) { if (imgdata && imgdata.fileName) {
profilepic = imgdata.fileName profilepic = imgdata.fileName
} }
} }
// Create a new user return profilepic
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}
}
}, },
Google: {
callback: async (user, data, ipAddress) => { callback: async (user, data, ipAddress) => {
let uid let uid
@ -312,56 +316,32 @@ const API = {
return {error: e.message} return {error: e.message}
} }
let exists = await API.Common.getExternal('google', uid) let cleanedData = Object.assign(data, {
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 = {
username: data.name.replace(/\W+/gi, ''), username: data.name.replace(/\W+/gi, ''),
display_name: data.name, display_name: data.name,
email: data.email || '', email: data.email || ''
avatar_file: profilepic, })
ip_address: ipAddress
}
let newUser = await API.Common.newUser('google', uid, newUData) return API.Common.callback('google', uid, user, ipAddress, cleanedData, API.Google.getAvatar)
if (!newUser) return {error: 'Failed to create user.'}
return {error: null, user: newUser}
} }
}, },
Discord: { 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 () { oauth2App: function () {
if (discordApp) return if (discordApp) return
discordApp = new oauth.PromiseOAuth2( discordApp = new oauth.PromiseOAuth2(
@ -374,9 +354,9 @@ const API = {
discordApp.useAuthorizationHeaderforGET(true) discordApp.useAuthorizationHeaderforGET(true)
}, },
getAuthorizeURL: function () { getAuthorizeURL: function (req) {
if (!discordApp) API.Discord.oauth2App() 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' let redirectUri = config.server.domain + '/api/external/discord/callback'
const params = { const params = {
@ -421,58 +401,15 @@ const API = {
} }
let uid = ddata.id 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 // Create a new user
let newUData = { let cleanedData = Object.assign(ddata, {
username: ddata.username.replace(/\W+/gi, '_'), username: ddata.username.replace(/\W+/gi, '_'),
display_name: ddata.username, display_name: ddata.username,
email: ddata.email || '', email: ddata.email || ''
avatar_file: profilepic, })
ip_address: ipAddress
}
let newUser = await API.Common.newUser('discord', uid, newUData) return API.Common.callback('discord', uid, user, ipAddress, cleanedData, API.Discord.getAvatar)
if (!newUser) return {error: 'Failed to create user.'}
return {error: null, user: newUser}
} }
} }
} }

View File

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

View File

@ -79,16 +79,34 @@ function JsonData (req, res, error, redirect = '/') {
res.jsonp({error: error, redirect: 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 /** FACEBOOK LOGIN
* Ajax POST only <in-page javascript handeled> * Ajax POST only <in-page javascript handeled>
* No tokens saved in configs, everything works out-of-the-box * No tokens saved in configs, everything works out-of-the-box
*/ */
router.post('/external/facebook/callback', wrap(async (req, res, next) => { router.post('/external/facebook/callback', wrap(async (req, res, next) => {
if (!config.facebook || !config.facebook.client) return 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) { if (response.banned) {
return JsonData(req, res, 'You are 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') JsonData(req, res, null, '/login')
})) }))
router.get('/external/facebook/remove', wrap(async (req, res) => { router.get('/external/facebook/remove', removeAuthMiddleware('facebook'))
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')
}))
/** TWITTER LOGIN /** TWITTER LOGIN
* OAuth1.0a flows * OAuth1.0a flows
@ -172,17 +181,7 @@ router.get('/external/twitter/callback', wrap(async (req, res) => {
res.render('redirect', {url: uri}) res.render('redirect', {url: uri})
})) }))
router.get('/external/twitter/remove', wrap(async (req, res) => { router.get('/external/twitter/remove', removeAuthMiddleware('twitter'))
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')
}))
/** DISCORD LOGIN /** DISCORD LOGIN
* OAuth2 flows * OAuth2 flows
@ -191,22 +190,16 @@ router.get('/external/twitter/remove', wrap(async (req, res) => {
router.get('/external/discord/login', wrap(async (req, res) => { router.get('/external/discord/login', wrap(async (req, res) => {
if (!config.discord || !config.discord.api) return res.redirect('/') if (!config.discord || !config.discord.api) return res.redirect('/')
let infos = APIExtern.Discord.getAuthorizeURL() let infos = APIExtern.Discord.getAuthorizeURL(req)
req.session.discord_auth = {
state: infos.state
}
res.redirect(infos.url) res.redirect(infos.url)
})) }))
router.get('/external/discord/callback', wrap(async (req, res) => { router.get('/external/discord/callback', wrap(async (req, res) => {
if (!config.discord || !config.discord.api) return res.redirect('/login') 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 code = req.query.code
let state = req.query.state let state = req.query.state
let da = req.session.discord_auth
let uri = '/login' let uri = '/login'
if (!code) { if (!code) {
@ -214,7 +207,7 @@ router.get('/external/discord/callback', wrap(async (req, res) => {
return res.redirect(uri) 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.'}) req.flash('message', {error: true, text: 'Request got intercepted, try again.'})
return res.redirect(uri) return res.redirect(uri)
} }
@ -245,29 +238,7 @@ router.get('/external/discord/callback', wrap(async (req, res) => {
res.render('redirect', {url: uri}) res.render('redirect', {url: uri})
})) }))
router.get('/external/discord/remove', wrap(async (req, res) => { router.get('/external/discord/remove', removeAuthMiddleware('discord'))
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')
}))
/** GOOGLE LOGIN /** GOOGLE LOGIN
* Google Token Verification * Google Token Verification
@ -303,17 +274,7 @@ router.post('/external/google/callback', wrap(async (req, res) => {
JsonData(req, res, null, '/login') JsonData(req, res, null, '/login')
})) }))
router.get('/external/google/remove', wrap(async (req, res) => { router.get('/external/google/remove', removeAuthMiddleware('google'))
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')
}))
/* ======== /* ========
* NEWS * NEWS

View File

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