diff --git a/package-lock.json b/package-lock.json index ba0d0f6..4f2d726 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 2f6c3c3..205a5c3 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/scripts/http.js b/scripts/http.js new file mode 100644 index 0000000..3c4c4fb --- /dev/null +++ b/scripts/http.js @@ -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 +} diff --git a/server/api/external.js b/server/api/external.js new file mode 100644 index 0000000..1595830 --- /dev/null +++ b/server/api/external.js @@ -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 diff --git a/server/api/index.js b/server/api/index.js index 7e96272..3d6ecbd 100644 --- a/server/api/index.js +++ b/server/api/index.js @@ -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 () { + + } } } diff --git a/server/routes/api.js b/server/routes/api.js new file mode 100644 index 0000000..88fbbf2 --- /dev/null +++ b/server/routes/api.js @@ -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 + * 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 diff --git a/server/routes/index.js b/server/routes/index.js index 99b21ca..6d8bceb 100644 --- a/server/routes/index.js +++ b/server/routes/index.js @@ -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() diff --git a/src/script/main.js b/src/script/main.js index 48f154c..27de1f1 100644 --- a/src/script/main.js +++ b/src/script/main.js @@ -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.') + }) + }) + } }) diff --git a/src/style/main.styl b/src/style/main.styl index 7510d33..e7aff6b 100644 --- a/src/style/main.styl +++ b/src/style/main.styl @@ -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 diff --git a/static/image/sign-in-with-twitter-link.png b/static/image/sign-in-with-twitter-link.png new file mode 100644 index 0000000..7dd94c7 Binary files /dev/null and b/static/image/sign-in-with-twitter-link.png differ diff --git a/views/includes/external.pug b/views/includes/external.pug new file mode 100644 index 0000000..a8683f0 --- /dev/null +++ b/views/includes/external.pug @@ -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") diff --git a/views/login.pug b/views/login.pug index 66ad54e..704db1b 100644 --- a/views/login.pug +++ b/views/login.pug @@ -24,3 +24,4 @@ block body a#create(href="/register") Create an account .right h3 More options + include includes/external.pug diff --git a/views/totp.pug b/views/totp.pug index 0da3277..12b04d8 100644 --- a/views/totp.pug +++ b/views/totp.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')