diff --git a/config.example.toml b/config.example.toml index 0673869..19f5469 100644 --- a/config.example.toml +++ b/config.example.toml @@ -57,6 +57,10 @@ # api="" # api_secret="" +[google] +# api="" +# api_secret="" + # reCAPTCHA configuration [security] [security.recaptcha] diff --git a/documents/privacy-policy.html b/documents/privacy-policy.html index 88d180c..e0f84c0 100644 --- a/documents/privacy-policy.html +++ b/documents/privacy-policy.html @@ -18,4 +18,6 @@

By logging in with Facebook, we will only ask you for your Public Profile and Email Address. We will use your Name as your Display Name, which can be changed from your Account Settings after logging in. Your profile picture may be downloaded onto our servers and used as your network-wide profile image. You may change your profile picture from your Account Settings at any time. Your Email Address will only be used to send you updates, which you can opt-out of. We can not: post on your behalf, see your friends list nor see your posts.

Discord

By logging in with Discord, we will only ask you for your Username and Email Address for the above-mentioned purposes. We do not ask you for any other information and we will not know which Discord Servers you're on.

+

Google

+

By logging in with Google, we will only ask you for your Name and Email Address for the above-mentioned purposes. We do not ask you for any other information and we will not have access to anything other than your public profile information.

diff --git a/server/api/external.js b/server/api/external.js index e45097b..4ef9c5c 100644 --- a/server/api/external.js +++ b/server/api/external.js @@ -292,6 +292,75 @@ const API = { return {error: null, user: newUser} } }, + Google: { + callback: async (user, data, ipAddress) => { + let uid + + try { + let test = await http.GET('https://www.googleapis.com/oauth2/v3/tokeninfo?id_token=' + data.id_token) + if (!test) throw new Error('No response!') + + let jsondata = JSON.parse(test) + if (!jsondata || !jsondata.email || !jsondata.name) throw new Error('Please allow Basic Profile and Email.') + + if (jsondata.email !== data.email || jsondata.name !== data.name) throw new Error('Conflicting data. Please try again!') + + if (new Date(parseInt(jsondata.exp) * 1000) < Date.now()) throw new Error('Expired token! Please try again!') + + uid = jsondata.sub + } catch (e) { + return {error: e.message} + } + + let exists = await API.Common.getExternal('google', uid) + + if (user) { + // Get bans for user + let bans = await API.Common.getBan(user) + if (bans.length) return { banned: bans, ip: false } + + if (exists) return {error: null, user: user} + + await API.Common.new('google', uid, user) + return {error: null, user: user} + } + + // Callback succeeded with user id and the external table exists, we log in the user + if (exists) { + // Get bans for user + let bans = await API.Common.getBan(exists.user) + if (bans.length) return { banned: bans, ip: false } + return {error: null, user: exists.user} + } + + // Get bans for IP + let bans = await API.Common.getBan(null, ipAddress) + if (bans.length) return { banned: bans, ip: true } + + // Determine profile picture + let profilepic = null + if (data.image) { + let imgdata = await API.Common.saveAvatar(data.image) + if (imgdata && imgdata.fileName) { + profilepic = imgdata.fileName + } + } + + // Create a new user + let newUData = { + username: data.name.replace(/\W+/gi, ''), + display_name: data.name, + email: data.email || '', + avatar_file: profilepic, + ip_address: ipAddress + } + + let newUser = await API.Common.newUser('google', uid, newUData) + if (!newUser) return {error: 'Failed to create user.'} + + return {error: null, user: newUser} + } + }, Discord: { oauth2App: function () { if (discordApp) return diff --git a/server/routes/api.js b/server/routes/api.js index c220fdd..89f2397 100644 --- a/server/routes/api.js +++ b/server/routes/api.js @@ -243,6 +243,64 @@ router.get('/external/discord/remove', wrap(async (req, res) => { res.redirect('/user/manage') })) +router.get('/external/discord/remove', wrap(async (req, res) => { + if (!req.session.user) return res.redirect('/login') + + let done = await APIExtern.Common.remove(req.session.user, 'discord') + + if (!done) { + req.flash('message', {error: true, text: 'Unable to unlink social media account'}) + } + + res.redirect('/user/manage') +})) + +/** GOOGLE LOGIN + * Google Token Verification + * Tokens in configs + */ +router.get('/external/google/login', wrap(async (req, res) => { + if (!config.google || !config.google.api) return res.redirect('/') + + res.redirect('/login') +})) + +router.post('/external/google/callback', wrap(async (req, res) => { + if (!config.google || !config.google.api) return res.redirect('/login') + + if (!req.body.id_token) { + return JsonData(req, res, 'Invalid or missing ID token!', '/login') + } + + let response = await APIExtern.Google.callback(req.session.user, req.body, req.realIP) + if (response.banned) { + return JsonData(req, res, 'Banned user.', '/login') + } + + if (response.error) { + return JsonData(req, res, response.error, '/login') + } + + if (!req.session.user) { + let user = response.user + createSession(req, user) + } + + JsonData(req, res, null, '/login') +})) + +router.get('/external/google/remove', wrap(async (req, res) => { + if (!req.session.user) return res.redirect('/login') + + let done = await APIExtern.Common.remove(req.session.user, 'google') + + if (!done) { + req.flash('message', {error: true, text: 'Unable to unlink social media account'}) + } + + res.redirect('/user/manage') +})) + /* ======== * NEWS * ======== diff --git a/server/routes/index.js b/server/routes/index.js index 9b23b27..82399dc 100644 --- a/server/routes/index.js +++ b/server/routes/index.js @@ -42,7 +42,6 @@ function setSession (req, user) { function redirectLogin (req, res) { let uri = '/' - console.log('goto', req.session.redirectUri) if (req.session.redirectUri) { uri = req.session.redirectUri delete req.session.redirectUri @@ -116,6 +115,10 @@ function extraButtons (req, res, next) { res.locals.facebook_auth = config.facebook.client } + if (config.google && config.google.api) { + res.locals.google_auth = config.google.api + } + next() } @@ -184,10 +187,11 @@ router.get('/user/manage', ensureLogin, wrap(async (req, res) => { totpEnabled = await API.User.Login.totpTokenRequired(req.session.user) } + // Decide whether we need a disconnect or a log in with button for social account logins if (config.twitter && config.twitter.api) { if (!socialStatus.enabled.twitter) { res.locals.twitter_auth = true - } else if (!socialStatus.source && socialStatus.source !== 'twitter') { + } else if (socialStatus.source !== 'twitter') { res.locals.twitter_auth = false } } @@ -195,7 +199,7 @@ router.get('/user/manage', ensureLogin, wrap(async (req, res) => { if (config.discord && config.discord.api) { if (!socialStatus.enabled.discord) { res.locals.discord_auth = true - } else if (!socialStatus.source && socialStatus.source !== 'discord') { + } else if (socialStatus.source !== 'discord') { res.locals.discord_auth = false } } @@ -203,11 +207,19 @@ router.get('/user/manage', ensureLogin, wrap(async (req, res) => { if (config.facebook && config.facebook.client) { if (!socialStatus.enabled.fb) { res.locals.facebook_auth = config.facebook.client - } else if (!socialStatus.source && socialStatus.source !== 'fb') { + } else if (socialStatus.source !== 'fb') { res.locals.facebook_auth = false } } + if (config.google && config.google.api) { + if (!socialStatus.enabled.google) { + res.locals.google_auth = config.google.api + } else if (socialStatus.source !== 'google') { + res.locals.google_auth = false + } + } + res.render('user/settings', {totp: totpEnabled, password: socialStatus.password}) })) diff --git a/src/script/main.js b/src/script/main.js index 2d64655..fa7cc44 100644 --- a/src/script/main.js +++ b/src/script/main.js @@ -267,20 +267,37 @@ $(document).ready(function () { data: response, success: function (data) { if (data.error) { - $('.message').addClass('error') - $('.message span').text(data.error) + alert(data.error) return } window.location.reload() } }).fail(function () { - $('.message').addClass('error') - $('.message span').text('An error occured.') + alert('An error occured.') }) }) } + window.googlePOST = function (response) { + $.ajax({ + type: 'post', + url: '/api/external/google/callback', + dataType: 'json', + data: response, + success: function (data) { + if (data.error) { + alert(data.error) + return + } + + window.location.reload() + } + }).fail(function () { + alert('An error occured.') + }) + } + $('.loginDiag').click(function (e) { e.preventDefault() var url = $(this).attr('href') diff --git a/src/style/main.styl b/src/style/main.styl index bbd555f..b6c8af1 100644 --- a/src/style/main.styl +++ b/src/style/main.styl @@ -274,14 +274,14 @@ input:not([type="submit"]) border-radius: 5px text-decoration: none i - color: #03A9F4; - font-size: 22px; + color: #03A9F4 + font-size: 22px span - color: #000; - display: inline-block; - vertical-align: top; - margin-top: 3px; - margin-left: 12px; + color: #000 + display: inline-block + vertical-align: top + margin-top: 3px + margin-left: 12px .discordLogin display: block @@ -302,6 +302,26 @@ input:not([type="submit"]) width: 45px display: inline-block +.googleLogin + padding: 10px + width: 215px + margin-top: 5px + background-color: #4285f4 + border: 1px solid #ddd + border-radius: 5px + text-decoration: none + display: inline-block + cursor: pointer + i + color: #fff + font-size: 22px + span + color: #fff + display: inline-block + vertical-align: top + margin-top: 3px + margin-left: 12px + .accdisconnect margin-top: 5px padding: 10px diff --git a/views/includes/external.pug b/views/includes/external.pug index 15fc178..3bfee73 100644 --- a/views/includes/external.pug +++ b/views/includes/external.pug @@ -20,6 +20,45 @@ 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 google_auth + script(src="https://apis.google.com/js/api:client.js") + a.googleLogin + i.fa.fa-fw.fa-google + span Log in With Google + script. + var googleUser = {}; + var startApp = function() { + gapi.load('auth2', function(){ + // Retrieve the singleton for the GoogleAuth library and set up the client. + auth2 = gapi.auth2.init({ + client_id: '#{google_auth}', + cookiepolicy: 'single_host_origin', + fetch_basic_profile: true + }); + attachSignin(document.querySelector('.googleLogin')); + }); + }; + + function attachSignin(element) { + auth2.attachClickHandler(element, {}, + function (googleUser) { + let profile = googleUser.getBasicProfile(); + let dataTree = { + id_token: googleUser.getAuthResponse().id_token, + name: profile.getName(), + email: profile.getEmail(), + image: profile.getImageUrl() + }; + + if (window.googlePOST) { + window.googlePOST(dataTree); + } + }, function(error) { + alert('Failed to log you in using Google.'); + }); + } + + startApp() if twitter_auth a.twitterLogin.loginDiag(href="/api/external/twitter/login") i.fa.fa-fw.fa-twitter diff --git a/views/user/settings.pug b/views/user/settings.pug index 30c6f51..ab84f12 100644 --- a/views/user/settings.pug +++ b/views/user/settings.pug @@ -33,6 +33,10 @@ block body h3 Social Media Accounts .specify(title="You can add social media accounts to your account for ease of login. Once added, logging in from linked sources logs you into this account automatically.") ? include ../includes/external.pug + if google_auth == false + a.option.accdisconnect(href="/api/external/google/remove") + i.fa.fa-fw.fa-times + |Unlink Google if twitter_auth == false a.option.accdisconnect(href="/api/external/twitter/remove") i.fa.fa-fw.fa-times