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==",
|
||||
"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": {
|
||||
"version": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz",
|
||||
"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": {
|
||||
"version": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
|
||||
@ -2922,6 +2941,18 @@
|
||||
"version": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz",
|
||||
"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": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||
|
@ -44,6 +44,7 @@
|
||||
"mysql": "^2.13.0",
|
||||
"nodemailer": "^4.0.1",
|
||||
"notp": "^2.0.3",
|
||||
"oauth-libre": "^0.9.17",
|
||||
"objection": "^0.8.4",
|
||||
"pug": "^2.0.0-rc.3",
|
||||
"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) {
|
||||
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)
|
||||
if (getToken && getToken.length) {
|
||||
await models.TotpToken.query().delete().where('user_id', user.id)
|
||||
@ -163,6 +168,7 @@ const API = {
|
||||
user_id: user.id,
|
||||
token: API.Hash(16),
|
||||
recovery_code: API.Hash(8),
|
||||
activated: 0,
|
||||
created_at: new Date()
|
||||
}
|
||||
|
||||
@ -215,6 +221,11 @@ const API = {
|
||||
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 API from '../api'
|
||||
|
||||
import apiRouter from './api'
|
||||
|
||||
let router = express.Router()
|
||||
|
||||
router.use(wrap(async (req, res, next) => {
|
||||
@ -38,6 +40,14 @@ router.get('/login', wrap(async (req, res) => {
|
||||
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')
|
||||
}))
|
||||
|
||||
@ -52,6 +62,13 @@ router.get('/register', wrap(async (req, res) => {
|
||||
}
|
||||
|
||||
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')
|
||||
}))
|
||||
@ -59,9 +76,10 @@ router.get('/register', wrap(async (req, res) => {
|
||||
router.get('/user/two-factor', wrap(async (req, res) => {
|
||||
if (!req.session.user) return res.redirect('/login')
|
||||
let twoFaEnabled = await API.User.Login.totpTokenRequired(req.session.user)
|
||||
|
||||
if (twoFaEnabled) return res.redirect('/')
|
||||
|
||||
let newToken = await API.User.Login.totpAquire(req.session.user)
|
||||
if (!newToken) return res.redirect('/')
|
||||
|
||||
res.locals.uri = newToken
|
||||
res.render('totp')
|
||||
@ -149,7 +167,8 @@ router.post('/login/verify', wrap(async (req, res) => {
|
||||
username: user.username,
|
||||
display_name: user.display_name,
|
||||
email: user.email,
|
||||
avatar_file: user.avatar_file
|
||||
avatar_file: user.avatar_file,
|
||||
session_refresh: Date.now() + 1800000 // 30 minutes
|
||||
}
|
||||
|
||||
let uri = '/'
|
||||
@ -197,7 +216,8 @@ router.post('/login', wrap(async (req, res) => {
|
||||
username: user.username,
|
||||
display_name: user.display_name,
|
||||
email: user.email,
|
||||
avatar_file: user.avatar_file
|
||||
avatar_file: user.avatar_file,
|
||||
session_refresh: Date.now() + 1800000 // 30 minutes
|
||||
}
|
||||
|
||||
let uri = '/'
|
||||
@ -300,6 +320,8 @@ router.get('/activate/:token', wrap(async (req, res) => {
|
||||
res.redirect('/login')
|
||||
}))
|
||||
|
||||
router.use('/api', apiRouter)
|
||||
|
||||
router.use((err, req, res, next) => {
|
||||
console.error(err)
|
||||
next()
|
||||
|
@ -34,4 +34,33 @@ $(document).ready(function () {
|
||||
scrollTop: dest - $('.navigator').innerHeight()
|
||||
}, 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
|
||||
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)
|
||||
.navigator
|
||||
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
|
||||
.right
|
||||
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
|
||||
h3 Authenticator app
|
||||
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