facebook, twitter and discord login methods
This commit is contained in:
parent
298f9bf818
commit
d178a8ee40
31
package-lock.json
generated
31
package-lock.json
generated
@ -419,6 +419,12 @@
|
|||||||
"integrity": "sha512-dwVUVIXsBZXwTuwnXI9RK8sBmgq09NDHzyR9SAph9eqk76gKK2JSQmZARC2zRC81JC2QTtxD0ARU5qTS25gIGw==",
|
"integrity": "sha512-dwVUVIXsBZXwTuwnXI9RK8sBmgq09NDHzyR9SAph9eqk76gKK2JSQmZARC2zRC81JC2QTtxD0ARU5qTS25gIGw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"basic-auth": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-1.1.0.tgz",
|
||||||
|
"integrity": "sha1-RSIe5Cn37h5QNb4/UVM/HN/SmIQ=",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
"bcryptjs": {
|
"bcryptjs": {
|
||||||
"version": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz",
|
"version": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz",
|
||||||
"integrity": "sha1-mrVie5PmBiH/fNrF2pczAn3x0Ms="
|
"integrity": "sha1-mrVie5PmBiH/fNrF2pczAn3x0Ms="
|
||||||
@ -2874,6 +2880,19 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"morgan": {
|
||||||
|
"version": "1.8.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/morgan/-/morgan-1.8.2.tgz",
|
||||||
|
"integrity": "sha1-eErHc05KRTqcbm6GgKkyknXItoc=",
|
||||||
|
"optional": true,
|
||||||
|
"requires": {
|
||||||
|
"basic-auth": "1.1.0",
|
||||||
|
"debug": "https://registry.npmjs.org/debug/-/debug-2.6.8.tgz",
|
||||||
|
"depd": "https://registry.npmjs.org/depd/-/depd-1.1.0.tgz",
|
||||||
|
"on-finished": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
|
||||||
|
"on-headers": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.1.tgz"
|
||||||
|
}
|
||||||
|
},
|
||||||
"ms": {
|
"ms": {
|
||||||
"version": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
"version": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||||
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
|
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
|
||||||
@ -2922,6 +2941,18 @@
|
|||||||
"version": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz",
|
"version": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz",
|
||||||
"integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0="
|
"integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0="
|
||||||
},
|
},
|
||||||
|
"oauth-libre": {
|
||||||
|
"version": "0.9.17",
|
||||||
|
"resolved": "https://registry.npmjs.org/oauth-libre/-/oauth-libre-0.9.17.tgz",
|
||||||
|
"integrity": "sha1-5Jg39iFj4QVj49BRNd82DcYYpxI=",
|
||||||
|
"requires": {
|
||||||
|
"bluebird": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.0.tgz",
|
||||||
|
"body-parser": "https://registry.npmjs.org/body-parser/-/body-parser-1.17.2.tgz",
|
||||||
|
"express": "https://registry.npmjs.org/express/-/express-4.15.3.tgz",
|
||||||
|
"express-session": "https://registry.npmjs.org/express-session/-/express-session-1.15.3.tgz",
|
||||||
|
"morgan": "1.8.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"object-assign": {
|
"object-assign": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||||
|
@ -44,6 +44,7 @@
|
|||||||
"mysql": "^2.13.0",
|
"mysql": "^2.13.0",
|
||||||
"nodemailer": "^4.0.1",
|
"nodemailer": "^4.0.1",
|
||||||
"notp": "^2.0.3",
|
"notp": "^2.0.3",
|
||||||
|
"oauth-libre": "^0.9.17",
|
||||||
"objection": "^0.8.4",
|
"objection": "^0.8.4",
|
||||||
"pug": "^2.0.0-rc.3",
|
"pug": "^2.0.0-rc.3",
|
||||||
"stylus": "^0.54.5",
|
"stylus": "^0.54.5",
|
||||||
|
111
scripts/http.js
Normal file
111
scripts/http.js
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
import url from 'url'
|
||||||
|
import qs from 'querystring'
|
||||||
|
|
||||||
|
function HTTP_GET (link, headers = {}, lback) {
|
||||||
|
if(lback && lback >= 4) throw new Error('infinite loop!') // Prevent infinite loop requests
|
||||||
|
let parsed = url.parse(link)
|
||||||
|
let opts = {
|
||||||
|
host: parsed.hostname,
|
||||||
|
port: parsed.port,
|
||||||
|
path: parsed.path,
|
||||||
|
headers: {
|
||||||
|
'User-Agent': 'Squeebot/Commons-2.0.0',
|
||||||
|
'Accept': '*/*',
|
||||||
|
'Accept-Language': 'en-GB,enq=0.5'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(headers) {
|
||||||
|
opts.headers = Object.assign(opts.headers, headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
let reqTimeOut
|
||||||
|
|
||||||
|
let httpModule = parsed.protocol === 'https:' ? require('https') : require('http')
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
let req = httpModule.get(opts, (res) => {
|
||||||
|
if (res.statusCode === 302 || res.statusCode === 301) {
|
||||||
|
if(!lback) {
|
||||||
|
lback = 1
|
||||||
|
} else {
|
||||||
|
lback += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return HTTP_GET(res.headers.location, headers, lback).then(resolve, reject)
|
||||||
|
}
|
||||||
|
|
||||||
|
let data = ''
|
||||||
|
|
||||||
|
reqTimeOut = setTimeout(() => {
|
||||||
|
req.abort()
|
||||||
|
data = null
|
||||||
|
reject(new Error('Request took too long!'))
|
||||||
|
}, 5000)
|
||||||
|
|
||||||
|
res.on('data', (chunk) => {
|
||||||
|
data += chunk
|
||||||
|
})
|
||||||
|
|
||||||
|
res.on('end', () => {
|
||||||
|
clearTimeout(reqTimeOut)
|
||||||
|
|
||||||
|
resolve(data)
|
||||||
|
})
|
||||||
|
}).on('error', (e) => {
|
||||||
|
reject(new Error(e.message))
|
||||||
|
})
|
||||||
|
|
||||||
|
req.setTimeout(10000)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function HTTP_POST (link, headers = {}, data) {
|
||||||
|
let parsed = url.parse(link)
|
||||||
|
let post_data = qs.stringify(data)
|
||||||
|
|
||||||
|
let opts = {
|
||||||
|
host: parsed.host,
|
||||||
|
port: parsed.port,
|
||||||
|
path: parsed.path,
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
'Content-Length': Buffer.byteLength(post_data),
|
||||||
|
'User-Agent': 'Squeebot/Commons-2.0.0'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (headers) {
|
||||||
|
opts.headers = Object.assign(opts.headers, headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.headers['Content-Type'] === 'application/json') {
|
||||||
|
post_data = JSON.stringify(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
let httpModule = parsed.protocol === 'https:' ? require('https') : require('http')
|
||||||
|
let req = httpModule.request(opts, (res) => {
|
||||||
|
res.setEncoding('utf8')
|
||||||
|
let data = ''
|
||||||
|
|
||||||
|
res.on('data', (chunk) => {
|
||||||
|
data += chunk
|
||||||
|
})
|
||||||
|
|
||||||
|
res.on('end', () => {
|
||||||
|
resolve(data)
|
||||||
|
})
|
||||||
|
}).on('error', (e) => {
|
||||||
|
reject(new Error(e))
|
||||||
|
})
|
||||||
|
|
||||||
|
req.write(post_data)
|
||||||
|
req.end()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
GET: HTTP_GET,
|
||||||
|
POST: HTTP_POST
|
||||||
|
}
|
346
server/api/external.js
Normal file
346
server/api/external.js
Normal file
@ -0,0 +1,346 @@
|
|||||||
|
import config from '../../scripts/load-config'
|
||||||
|
import database from '../../scripts/load-database'
|
||||||
|
import http from '../../scripts/http'
|
||||||
|
import models from './models'
|
||||||
|
import UAPI from './index'
|
||||||
|
import qs from 'querystring'
|
||||||
|
import oauth from 'oauth-libre'
|
||||||
|
|
||||||
|
let twitterApp
|
||||||
|
let discordApp
|
||||||
|
|
||||||
|
const API = {
|
||||||
|
Common: {
|
||||||
|
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) {
|
||||||
|
let user = await UAPI.User.get(extr.user_id)
|
||||||
|
if (user) {
|
||||||
|
extr.user = user
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return extr
|
||||||
|
},
|
||||||
|
new: async (service, identifier, user) => {
|
||||||
|
let data = {
|
||||||
|
user_id: user.id,
|
||||||
|
service: service,
|
||||||
|
identifier: identifier,
|
||||||
|
created_at: new Date()
|
||||||
|
}
|
||||||
|
|
||||||
|
await await models.External.query().insert(data)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Facebook: {
|
||||||
|
callback: async (user, data) => {
|
||||||
|
if (!data.authResponse || data.status !== 'connected') {
|
||||||
|
return {error: 'No Authorization'}
|
||||||
|
}
|
||||||
|
|
||||||
|
let uid = data.authResponse.userID
|
||||||
|
if (!uid) {
|
||||||
|
return {error: 'No Authorization'}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get facebook user information in order to create a new user or verify
|
||||||
|
let fbdata
|
||||||
|
let intel = {
|
||||||
|
access_token: data.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) {
|
||||||
|
return {error: 'Could not get user information', errorObject: e}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fbdata.error) {
|
||||||
|
return {error: fbdata.error.message}
|
||||||
|
}
|
||||||
|
|
||||||
|
let exists = await API.Common.getExternal('fb', uid)
|
||||||
|
|
||||||
|
if (user) {
|
||||||
|
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
|
||||||
|
if (exists) {
|
||||||
|
return {error: null, user: exists.user}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine profile picture
|
||||||
|
let profilepic = ''
|
||||||
|
if (fbdata.picture) {
|
||||||
|
if (fbdata.picture.is_silhouette == false && fbdata.picture.url) {
|
||||||
|
// TODO: Download the profile image and save it locally
|
||||||
|
profilepic = fbdata.picture.url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new user
|
||||||
|
let udataLimited = {
|
||||||
|
username: fbdata.short_name || 'FB' + UAPI.Hash(4),
|
||||||
|
display_name: fbdata.name,
|
||||||
|
email: fbdata.email || '',
|
||||||
|
avatar_file: profilepic,
|
||||||
|
activated: 1,
|
||||||
|
ip_address: data.ip_address,
|
||||||
|
created_at: new Date()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the username is already taken
|
||||||
|
if (await UAPI.User.get(udataLimited.username) != null) {
|
||||||
|
udataLimited.username = 'FB' + UAPI.Hash(4)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the email Facebook gave us is already registered, if so,
|
||||||
|
// associate an external node with the user bearing the email
|
||||||
|
if (udataLimited.email && udataLimited.email !== '') {
|
||||||
|
let getByEmail = await UAPI.User.get(udataLimited.email)
|
||||||
|
if (getByEmail) {
|
||||||
|
await API.Common.new('fb', getByEmail.id, getByEmail)
|
||||||
|
return {error: null, user: getByEmail}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new user based on the information we got from Facebook
|
||||||
|
let newUser = await models.User.query().insert(udataLimited)
|
||||||
|
await API.Common.new('fb', uid, newUser)
|
||||||
|
|
||||||
|
return {error: null, user: newUser}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Twitter: {
|
||||||
|
oauthApp: function () {
|
||||||
|
if (!twitterApp) {
|
||||||
|
let 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.twitter.api,
|
||||||
|
config.twitter.api_secret,
|
||||||
|
'1.0A',
|
||||||
|
redirectUri,
|
||||||
|
'HMAC-SHA1'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getRequestToken: async function () {
|
||||||
|
if (!twitterApp) API.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]}
|
||||||
|
},
|
||||||
|
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)
|
||||||
|
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]}
|
||||||
|
},
|
||||||
|
callback: async function (user, accessTokens, ipAddress) {
|
||||||
|
if (!twitterApp) API.Twitter.oauthApp()
|
||||||
|
let twdata
|
||||||
|
try {
|
||||||
|
let 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.'}
|
||||||
|
}
|
||||||
|
|
||||||
|
let uid = twdata.id_str
|
||||||
|
let exists = await API.Common.getExternal('twitter', uid)
|
||||||
|
|
||||||
|
if (user) {
|
||||||
|
if (exists) return {error: null, user: user}
|
||||||
|
|
||||||
|
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
|
||||||
|
if (exists) {
|
||||||
|
return {error: null, user: exists.user}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine profile picture
|
||||||
|
let profilepic = ''
|
||||||
|
if (twdata.profile_image_url_https) {
|
||||||
|
// TODO: Download the profile image and save it locally
|
||||||
|
profilepic = twdata.profile_image_url_https
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new user
|
||||||
|
let udataLimited = {
|
||||||
|
username: twdata.screen_name,
|
||||||
|
display_name: twdata.name,
|
||||||
|
email: twdata.email || '',
|
||||||
|
avatar_file: profilepic,
|
||||||
|
activated: 1,
|
||||||
|
ip_address: ipAddress,
|
||||||
|
created_at: new Date()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the username is already taken
|
||||||
|
if (await UAPI.User.get(udataLimited.username) != null) {
|
||||||
|
udataLimited.username = 'Tw' + UAPI.Hash(4)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the email Twitter gave us is already registered, if so,
|
||||||
|
// associate an external node with the user bearing the email
|
||||||
|
if (udataLimited.email && udataLimited.email !== '') {
|
||||||
|
let getByEmail = await UAPI.User.get(udataLimited.email)
|
||||||
|
if (getByEmail) {
|
||||||
|
await API.Common.new('twitter', getByEmail.id, getByEmail)
|
||||||
|
return {error: null, user: getByEmail}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new user based on the information we got from Twitter
|
||||||
|
let newUser = await models.User.query().insert(udataLimited)
|
||||||
|
await API.Common.new('twitter', uid, newUser)
|
||||||
|
|
||||||
|
return {error: null, user: newUser}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Discord: {
|
||||||
|
oauth2App: function() {
|
||||||
|
if (discordApp) return
|
||||||
|
discordApp = new oauth.PromiseOAuth2(
|
||||||
|
config.discord.api,
|
||||||
|
config.discord.api_secret,
|
||||||
|
'https://discordapp.com/api/',
|
||||||
|
'oauth2/authorize',
|
||||||
|
'oauth2/token'
|
||||||
|
)
|
||||||
|
|
||||||
|
discordApp.useAuthorizationHeaderforGET(true)
|
||||||
|
},
|
||||||
|
getAuthorizeURL: function () {
|
||||||
|
if (!discordApp) API.Discord.oauth2App()
|
||||||
|
let state = UAPI.Hash(6)
|
||||||
|
let redirectUri = config.server.domain + '/api/external/discord/callback'
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
'client_id': config.discord.api,
|
||||||
|
'redirect_uri': redirectUri,
|
||||||
|
'scope': 'identify email',
|
||||||
|
'response_type': 'code',
|
||||||
|
'state': state
|
||||||
|
}
|
||||||
|
|
||||||
|
let url = discordApp.getAuthorizeUrl(params)
|
||||||
|
|
||||||
|
return {error: null, state: state, url: url}
|
||||||
|
},
|
||||||
|
getAccessToken: async function (code) {
|
||||||
|
if (!discordApp) API.Discord.oauth2App()
|
||||||
|
|
||||||
|
let 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}
|
||||||
|
},
|
||||||
|
callback: async function (user, accessToken, ipAddress) {
|
||||||
|
if (!discordApp) API.Discord.oauth2App()
|
||||||
|
|
||||||
|
let ddata
|
||||||
|
try {
|
||||||
|
let 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'}
|
||||||
|
}
|
||||||
|
|
||||||
|
let uid = ddata.id
|
||||||
|
let exists = await API.Common.getExternal('discord', uid)
|
||||||
|
|
||||||
|
if (user) {
|
||||||
|
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) {
|
||||||
|
return {error: null, user: exists.user}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine profile picture
|
||||||
|
let profilepic = ''
|
||||||
|
// TODO: Download the profile image and save it locally
|
||||||
|
|
||||||
|
// Create a new user
|
||||||
|
let udataLimited = {
|
||||||
|
username: 'D' + ddata.discriminator,
|
||||||
|
display_name: ddata.username,
|
||||||
|
email: ddata.email || '',
|
||||||
|
avatar_file: profilepic,
|
||||||
|
activated: 1,
|
||||||
|
ip_address: ipAddress,
|
||||||
|
created_at: new Date()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the email Discord gave us is already registered, if so,
|
||||||
|
// associate an external node with the user bearing the email
|
||||||
|
if (udataLimited.email && udataLimited.email !== '') {
|
||||||
|
let getByEmail = await UAPI.User.get(udataLimited.email)
|
||||||
|
if (getByEmail) {
|
||||||
|
await API.Common.new('discord', uid, getByEmail)
|
||||||
|
return {error: null, user: getByEmail}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new user based on the information we got from Discord
|
||||||
|
let newUser = await models.User.query().insert(udataLimited)
|
||||||
|
await API.Common.new('discord', uid, newUser)
|
||||||
|
|
||||||
|
return {error: null, user: newUser}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = API
|
@ -154,6 +154,11 @@ const API = {
|
|||||||
},
|
},
|
||||||
totpAquire: async function (user) {
|
totpAquire: async function (user) {
|
||||||
user = await API.User.ensureObject(user)
|
user = await API.User.ensureObject(user)
|
||||||
|
|
||||||
|
// Do not allow totp for users who have registered using an external service
|
||||||
|
if (!user.password || user.password === '') return null
|
||||||
|
|
||||||
|
// Get existing tokens for the user and delete them if found
|
||||||
let getToken = await models.TotpToken.query().where('user_id', user.id)
|
let getToken = await models.TotpToken.query().where('user_id', user.id)
|
||||||
if (getToken && getToken.length) {
|
if (getToken && getToken.length) {
|
||||||
await models.TotpToken.query().delete().where('user_id', user.id)
|
await models.TotpToken.query().delete().where('user_id', user.id)
|
||||||
@ -163,6 +168,7 @@ const API = {
|
|||||||
user_id: user.id,
|
user_id: user.id,
|
||||||
token: API.Hash(16),
|
token: API.Hash(16),
|
||||||
recovery_code: API.Hash(8),
|
recovery_code: API.Hash(8),
|
||||||
|
activated: 0,
|
||||||
created_at: new Date()
|
created_at: new Date()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -215,6 +221,11 @@ const API = {
|
|||||||
return {error: null, user: user}
|
return {error: null, user: user}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
External: {
|
||||||
|
serviceCallback: async function () {
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
200
server/routes/api.js
Normal file
200
server/routes/api.js
Normal file
@ -0,0 +1,200 @@
|
|||||||
|
import express from 'express'
|
||||||
|
import parseurl from 'parseurl'
|
||||||
|
import config from '../../scripts/load-config'
|
||||||
|
import wrap from '../../scripts/asyncRoute'
|
||||||
|
import API from '../api'
|
||||||
|
import APIExtern from '../api/external'
|
||||||
|
|
||||||
|
let router = express.Router()
|
||||||
|
|
||||||
|
// Turn things like 'key1[key2]': 'value' into key1: {key2: 'value'} because facebook
|
||||||
|
function objectAssembler (insane) {
|
||||||
|
let object = {}
|
||||||
|
for (let key in insane) {
|
||||||
|
let value = insane[key]
|
||||||
|
if (key.indexOf('[') !== -1) {
|
||||||
|
let subKey = key.match(/^([\w]+)\[(\w+)\]$/)
|
||||||
|
if (subKey[1] && subKey[2]) {
|
||||||
|
if (!object[subKey[1]]) {
|
||||||
|
object[subKey[1]] = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
object[subKey[1]][subKey[2]] = value
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
object[key] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return object
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a session and return a redirect uri if provided
|
||||||
|
function createSession (req, user) {
|
||||||
|
let uri = '/'
|
||||||
|
req.session.user = {
|
||||||
|
id: user.id,
|
||||||
|
username: user.username,
|
||||||
|
display_name: user.display_name,
|
||||||
|
email: user.email,
|
||||||
|
avatar_file: user.avatar_file,
|
||||||
|
session_refresh: Date.now() + 1800000 // 30 minutes
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.session.redirectUri) {
|
||||||
|
uri = req.session.redirectUri
|
||||||
|
delete req.session.redirectUri
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.query.redirect) {
|
||||||
|
uri = req.query.redirect
|
||||||
|
}
|
||||||
|
|
||||||
|
return uri
|
||||||
|
}
|
||||||
|
|
||||||
|
// Either give JSON or make a redirect
|
||||||
|
function JsonData (req, res, error, redirect='/') {
|
||||||
|
if (req.headers['content-type'] == 'application/json') {
|
||||||
|
return res.jsonp({error: error, redirect: redirect})
|
||||||
|
}
|
||||||
|
|
||||||
|
req.flash('message', {error: true, text: error})
|
||||||
|
res.redirect(redirect)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 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) => {
|
||||||
|
let sane = objectAssembler(req.body)
|
||||||
|
sane.ip_address = req.realIP
|
||||||
|
|
||||||
|
let response = await APIExtern.Facebook.callback(req.session.user, sane)
|
||||||
|
|
||||||
|
if (response.error) {
|
||||||
|
return JsonData(req, res, response.error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create session
|
||||||
|
let uri = '/'
|
||||||
|
if (!req.session.user) {
|
||||||
|
let user = response.user
|
||||||
|
uri = createSession(req, user)
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonData(req, res, null, uri)
|
||||||
|
}))
|
||||||
|
|
||||||
|
/** TWITTER LOGIN
|
||||||
|
* OAuth1.0a flows
|
||||||
|
* Tokens in configs
|
||||||
|
*/
|
||||||
|
router.get('/external/twitter/login', wrap(async (req, res) => {
|
||||||
|
if (!config.twitter || !config.twitter.api) return res.redirect('/')
|
||||||
|
let tokens = await APIExtern.Twitter.getRequestToken()
|
||||||
|
|
||||||
|
if (tokens.error) {
|
||||||
|
return res.jsonp({error: tokens.error})
|
||||||
|
}
|
||||||
|
|
||||||
|
req.session.twitter_auth = tokens
|
||||||
|
if (req.query.returnTo) {
|
||||||
|
req.session.twitter_auth.returnTo = req.query.returnTo
|
||||||
|
}
|
||||||
|
|
||||||
|
res.redirect('https://twitter.com/oauth/authenticate?oauth_token=' + tokens.token)
|
||||||
|
}))
|
||||||
|
|
||||||
|
router.get('/external/twitter/callback', wrap(async (req, res) => {
|
||||||
|
if (!config.twitter || !config.twitter.api) return res.redirect('/login')
|
||||||
|
if (!req.session.twitter_auth) return res.redirect('/login')
|
||||||
|
let ta = req.session.twitter_auth
|
||||||
|
let uri = ta.returnTo || '/login'
|
||||||
|
|
||||||
|
if (!req.query.oauth_verifier) {
|
||||||
|
req.flash('message', {error: true, text: 'Couldn\'t get a verifier'})
|
||||||
|
return res.redirect(uri)
|
||||||
|
}
|
||||||
|
|
||||||
|
let accessTokens = await APIExtern.Twitter.getAccessTokens(ta.token, ta.token_secret, req.query.oauth_verifier)
|
||||||
|
delete req.session.twitter_auth
|
||||||
|
|
||||||
|
if (accessTokens.error) {
|
||||||
|
req.flash('message', {error: true, text: 'Couldn\'t get an access token'})
|
||||||
|
return res.redirect(uri)
|
||||||
|
}
|
||||||
|
|
||||||
|
let response = await APIExtern.Twitter.callback(req.session.user, accessTokens, req.realIP)
|
||||||
|
if (response.error) {
|
||||||
|
req.flash('message', {error: true, text: response.error})
|
||||||
|
return res.redirect(uri)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!req.session.user) {
|
||||||
|
let user = response.user
|
||||||
|
uri = createSession(req, user)
|
||||||
|
}
|
||||||
|
|
||||||
|
res.redirect(uri)
|
||||||
|
}))
|
||||||
|
|
||||||
|
/** DISCORD LOGIN
|
||||||
|
* OAuth2 flows
|
||||||
|
* Tokens in configs
|
||||||
|
*/
|
||||||
|
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 = {
|
||||||
|
returnTo: req.query.returnTo || '/login',
|
||||||
|
state: infos.state
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = da.returnTo || '/login'
|
||||||
|
|
||||||
|
if (!code) {
|
||||||
|
req.flash('message', {error: true, text: 'No authorization.'})
|
||||||
|
return res.redirect(uri)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!state || state !== da.state) {
|
||||||
|
req.flash('message', {error: true, text: 'Request got intercepted, try again.'})
|
||||||
|
return res.redirect(uri)
|
||||||
|
}
|
||||||
|
|
||||||
|
delete req.session.discord_auth
|
||||||
|
|
||||||
|
let accessToken = await APIExtern.Discord.getAccessToken(code)
|
||||||
|
if (accessToken.error) {
|
||||||
|
req.flash('message', {error: true, text: accessToken.error})
|
||||||
|
return res.redirect(uri)
|
||||||
|
}
|
||||||
|
|
||||||
|
let response = await APIExtern.Discord.callback(req.session.user, accessToken.accessToken, req.realIP)
|
||||||
|
if (response.error) {
|
||||||
|
req.flash('message', {error: true, text: response.error})
|
||||||
|
return res.redirect(uri)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!req.session.user) {
|
||||||
|
let user = response.user
|
||||||
|
uri = createSession(req, user)
|
||||||
|
}
|
||||||
|
|
||||||
|
res.redirect(uri)
|
||||||
|
}))
|
||||||
|
|
||||||
|
module.exports = router
|
@ -4,6 +4,8 @@ import config from '../../scripts/load-config'
|
|||||||
import wrap from '../../scripts/asyncRoute'
|
import wrap from '../../scripts/asyncRoute'
|
||||||
import API from '../api'
|
import API from '../api'
|
||||||
|
|
||||||
|
import apiRouter from './api'
|
||||||
|
|
||||||
let router = express.Router()
|
let router = express.Router()
|
||||||
|
|
||||||
router.use(wrap(async (req, res, next) => {
|
router.use(wrap(async (req, res, next) => {
|
||||||
@ -38,6 +40,14 @@ router.get('/login', wrap(async (req, res) => {
|
|||||||
return res.redirect(uri)
|
return res.redirect(uri)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (config.twitter && config.twitter.api) {
|
||||||
|
res.locals.twitter_auth = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.facebook && config.facebook.client) {
|
||||||
|
res.locals.facebook_auth = config.facebook.client
|
||||||
|
}
|
||||||
|
|
||||||
res.render('login')
|
res.render('login')
|
||||||
}))
|
}))
|
||||||
|
|
||||||
@ -52,6 +62,13 @@ router.get('/register', wrap(async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
res.locals.formkeep = dataSave
|
res.locals.formkeep = dataSave
|
||||||
|
if (config.twitter && config.twitter.api) {
|
||||||
|
res.locals.twitter_auth = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.facebook && config.facebook.client) {
|
||||||
|
res.locals.facebook_auth = config.facebook.client
|
||||||
|
}
|
||||||
|
|
||||||
res.render('register')
|
res.render('register')
|
||||||
}))
|
}))
|
||||||
@ -59,9 +76,10 @@ router.get('/register', wrap(async (req, res) => {
|
|||||||
router.get('/user/two-factor', wrap(async (req, res) => {
|
router.get('/user/two-factor', wrap(async (req, res) => {
|
||||||
if (!req.session.user) return res.redirect('/login')
|
if (!req.session.user) return res.redirect('/login')
|
||||||
let twoFaEnabled = await API.User.Login.totpTokenRequired(req.session.user)
|
let twoFaEnabled = await API.User.Login.totpTokenRequired(req.session.user)
|
||||||
|
|
||||||
if (twoFaEnabled) return res.redirect('/')
|
if (twoFaEnabled) return res.redirect('/')
|
||||||
|
|
||||||
let newToken = await API.User.Login.totpAquire(req.session.user)
|
let newToken = await API.User.Login.totpAquire(req.session.user)
|
||||||
|
if (!newToken) return res.redirect('/')
|
||||||
|
|
||||||
res.locals.uri = newToken
|
res.locals.uri = newToken
|
||||||
res.render('totp')
|
res.render('totp')
|
||||||
@ -149,7 +167,8 @@ router.post('/login/verify', wrap(async (req, res) => {
|
|||||||
username: user.username,
|
username: user.username,
|
||||||
display_name: user.display_name,
|
display_name: user.display_name,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
avatar_file: user.avatar_file
|
avatar_file: user.avatar_file,
|
||||||
|
session_refresh: Date.now() + 1800000 // 30 minutes
|
||||||
}
|
}
|
||||||
|
|
||||||
let uri = '/'
|
let uri = '/'
|
||||||
@ -197,7 +216,8 @@ router.post('/login', wrap(async (req, res) => {
|
|||||||
username: user.username,
|
username: user.username,
|
||||||
display_name: user.display_name,
|
display_name: user.display_name,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
avatar_file: user.avatar_file
|
avatar_file: user.avatar_file,
|
||||||
|
session_refresh: Date.now() + 1800000 // 30 minutes
|
||||||
}
|
}
|
||||||
|
|
||||||
let uri = '/'
|
let uri = '/'
|
||||||
@ -300,6 +320,8 @@ router.get('/activate/:token', wrap(async (req, res) => {
|
|||||||
res.redirect('/login')
|
res.redirect('/login')
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
router.use('/api', apiRouter)
|
||||||
|
|
||||||
router.use((err, req, res, next) => {
|
router.use((err, req, res, next) => {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
next()
|
next()
|
||||||
|
@ -34,4 +34,33 @@ $(document).ready(function () {
|
|||||||
scrollTop: dest - $('.navigator').innerHeight()
|
scrollTop: dest - $('.navigator').innerHeight()
|
||||||
}, 1000, 'swing')
|
}, 1000, 'swing')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
window.checkLoginState = function () {
|
||||||
|
FB.getLoginStatus(function(response) {
|
||||||
|
console.log(response)
|
||||||
|
$.ajax({
|
||||||
|
type: 'post',
|
||||||
|
url: '/api/external/facebook/callback',
|
||||||
|
dataType: 'json',
|
||||||
|
data: response,
|
||||||
|
success: (data) => {
|
||||||
|
if (data.error) {
|
||||||
|
console.log(data)
|
||||||
|
$('.message').addClass('error')
|
||||||
|
$('.message span').text(data.error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.redirect) {
|
||||||
|
return window.location.href = data.redirect
|
||||||
|
}
|
||||||
|
|
||||||
|
window.location.reload()
|
||||||
|
}
|
||||||
|
}).fail(function() {
|
||||||
|
$('.message').addClass('error')
|
||||||
|
$('.message span').text('An error occured.')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
@ -140,6 +140,24 @@ input[type="submit"]
|
|||||||
h1, h2, h3
|
h1, h2, h3
|
||||||
margin-top: 0
|
margin-top: 0
|
||||||
|
|
||||||
|
.dlbtn
|
||||||
|
display: block
|
||||||
|
img
|
||||||
|
width: 200px
|
||||||
|
&.apple
|
||||||
|
img
|
||||||
|
width: 175px
|
||||||
|
padding: 0 12px
|
||||||
|
|
||||||
|
.twitterLogin
|
||||||
|
display: inline-block
|
||||||
|
padding: 10px
|
||||||
|
width: 215px
|
||||||
|
margin: 5px 0
|
||||||
|
background-color: #fff
|
||||||
|
border: 1px solid #ddd
|
||||||
|
border-radius: 5px
|
||||||
|
|
||||||
@media all and (max-width: 800px)
|
@media all and (max-width: 800px)
|
||||||
.navigator
|
.navigator
|
||||||
padding: 0 10px
|
padding: 0 10px
|
||||||
|
BIN
static/image/sign-in-with-twitter-link.png
Normal file
BIN
static/image/sign-in-with-twitter-link.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.6 KiB |
25
views/includes/external.pug
Normal file
25
views/includes/external.pug
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
.external-login
|
||||||
|
if facebook_auth
|
||||||
|
div#fb-root
|
||||||
|
script.
|
||||||
|
window.fbAsyncInit = function() {
|
||||||
|
FB.init({
|
||||||
|
appId : '#{facebook_auth}',
|
||||||
|
cookie : true,
|
||||||
|
xfbml : true,
|
||||||
|
version : 'v2.8'
|
||||||
|
});
|
||||||
|
FB.AppEvents.logPageView();
|
||||||
|
};
|
||||||
|
|
||||||
|
(function(d, s, id) {
|
||||||
|
var js, fjs = d.getElementsByTagName(s)[0];
|
||||||
|
if (d.getElementById(id)) return;
|
||||||
|
js = d.createElement(s); js.id = id;
|
||||||
|
js.src = "//connect.facebook.net/en_US/sdk.js#xfbml=1&version=v2.10&appId=1124948240960869";
|
||||||
|
fjs.parentNode.insertBefore(js, fjs);
|
||||||
|
}(document, 'script', 'facebook-jssdk'));
|
||||||
|
fb:login-button(scope="public_profile,email", onlogin="checkLoginState();" data-max-rows="1", data-size="large", data-button-type="login_with", data-show-faces="false", data-auto-logout-link="false", data-use-continue-as="false")
|
||||||
|
if twitter_auth
|
||||||
|
a.twitterLogin(href="/api/external/twitter/login")
|
||||||
|
img(src="/static/image/sign-in-with-twitter-link.png")
|
@ -24,3 +24,4 @@ block body
|
|||||||
a#create(href="/register") Create an account
|
a#create(href="/register") Create an account
|
||||||
.right
|
.right
|
||||||
h3 More options
|
h3 More options
|
||||||
|
include includes/external.pug
|
||||||
|
@ -28,3 +28,7 @@ block body
|
|||||||
li You will now be asked for a code every time you log in
|
li You will now be asked for a code every time you log in
|
||||||
h3 Authenticator app
|
h3 Authenticator app
|
||||||
p We recommend using Google Authenticator
|
p We recommend using Google Authenticator
|
||||||
|
a.dlbtn(href='https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2&pcampaignid=MKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1', target="_blank")
|
||||||
|
img(alt='Get it on Google Play' src='https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png')
|
||||||
|
a.dlbtn.apple(href='https://itunes.apple.com/us/app/google-authenticator/id388497605', target="_blank")
|
||||||
|
img(alt='Download on the App Store' src='https://devimages-cdn.apple.com/app-store/marketing/guidelines/images/badge-download-on-the-app-store.svg')
|
||||||
|
Reference in New Issue
Block a user