From 553a2505e6aa1e9cd371ddd64395ee2739de49f4 Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 31 Jan 2019 16:52:18 -0500 Subject: [PATCH] add google and github oauth2 login support --- api/controllers/AuthController.js | 2 ++ api/helpers/passport.js | 33 +++++++++++++++++++----------- assets/js/actions/login.js | 4 ++-- assets/js/actions/targets.js | 18 ++++++++++++---- assets/js/containers/Carousel.js | 6 ++++-- assets/js/lib/Ajax.js | 11 +++++----- assets/js/login.js | 6 ++++-- assets/js/reducers/targets.js | 7 +++++++ assets/js/targets.js | 6 ++++-- assets/styles/lib/vars.scss | 6 +++--- assets/styles/shared/carousel.scss | 3 +++ config/env/development.js | 2 +- config/env/production.js | 2 +- config/passport.js | 12 +++++++++++ config/protocols.js | 17 +++++++++++++++ 15 files changed, 101 insertions(+), 34 deletions(-) diff --git a/api/controllers/AuthController.js b/api/controllers/AuthController.js index f1d1ac9..502afca 100644 --- a/api/controllers/AuthController.js +++ b/api/controllers/AuthController.js @@ -134,6 +134,8 @@ module.exports = { // redirect if there is a 'next' param if (req.query.next) { res.status(302).set('Location', req.query.next) + } else if (req.query.code) { // if came from oauth callback + res.status(302).set('Location', '/app') } sails.log.info('user', user, 'authenticated successfully') diff --git a/api/helpers/passport.js b/api/helpers/passport.js index c034f07..0879682 100644 --- a/api/helpers/passport.js +++ b/api/helpers/passport.js @@ -48,8 +48,8 @@ function PassportHelper () { const protocol = strategies[key].protocol const callbackURL = strategies[key].callback let baseURL = '' - if (sails.config.appUrl && sails.config.appUrl !== null) { - baseURL = sails.config.appUrl + if (sails.config.custom.baseURL && sails.config.custom.baseURL !== null) { + baseURL = sails.config.custom.baseURL } else { sails.log.warn('Please add \'appUrl\' to configuration') baseURL = sails.getBaseurl() @@ -74,7 +74,12 @@ function PassportHelper () { if (!_.has(strategies, provider)) return res.redirect('/login') - passport.authenticate(provider, {})(req, res, req.next) + const scopes = { + google: ['email'], + github: ['user:email'] + } + + passport.authenticate(provider, { scope: scopes[provider] })(req, res, req.next) } // a callback helper to split by req this.callback = function (req, res, next) { @@ -116,29 +121,33 @@ function PassportHelper () { const pass = await Passport.findOne({ provider, - identifier: q.identifier.toString() + identifier: q.identifier }) let user if (!req.user) { - if (!pass) { // new user signing up, create a new user - user = await User.create(userAttrs).fetch() + if (!pass) { // new user signing up, create a new user and/or passport + user = await User.findOne({ email: userAttrs.email }) + if (!user) { + user = await User.create(userAttrs).fetch() + } await Passport.create({ ...q, user: user.id }) next(null, user) } else { // existing user logging in - if (_.has(q, 'tokens') && q.tokens !== passport.tokens) { - passport.tokens = q.tokens + if (_.has(q, 'tokens') && q.tokens !== pass.tokens) { + pass.tokens = q.tokens } - await passport.save() - user = User.findOne(passport.user) - next(null, user) + delete pass.id + await Passport.update({ id: pass.id }, { tokens: pass.tokens }) + user = await User.find({ id: passport.user }).limit(1) + next(null, user[0]) } } else { // user logged in and trying to add new Passport - if (!passport) { + if (!pass) { await Passport.create({ ...q, user: req.user.id diff --git a/assets/js/actions/login.js b/assets/js/actions/login.js index 5d690f1..1976253 100644 --- a/assets/js/actions/login.js +++ b/assets/js/actions/login.js @@ -94,7 +94,7 @@ export const checkPassword = (email, password) => async (dispatch, getState) => } catch (e) { dispatch(setError({ type: 'password', - error: e.toString() + error: e.message })) dispatch(setWorking(false)) } @@ -122,7 +122,7 @@ export const signup = (email, password) => async (dispatch, getState) => { } catch (e) { dispatch(setError({ type: 'email', - error: e.toString() + error: e.message })) } } else { diff --git a/assets/js/actions/targets.js b/assets/js/actions/targets.js index 9c6fe56..ddaffdc 100644 --- a/assets/js/actions/targets.js +++ b/assets/js/actions/targets.js @@ -9,7 +9,8 @@ const ACTIONS = { delete_url: 'delete_url', list_url: 'list_url', set_editing: 'set_editing', - error: 'error' + error: 'error', + set_user: 'set_user' } export default ACTIONS @@ -24,6 +25,11 @@ export const setUrls = (urls) => ({ data: urls }) +export const setUser = user => ({ + type: ACTIONS.set_user, + data: user +}) + export const addUrl = url => ({ type: ACTIONS.add_url, data: url @@ -63,13 +69,17 @@ export const removeUrl = id => async (dispatch, getState) => { } } -export const fetchUrls = () => async (dispatch, getState) => { +export const fetchData = () => async (dispatch, getState) => { dispatch(setWorking(true)) try { - const { data } = await Ajax.get({ + const { data: user } = await Ajax.get({ + url: '/api/me' + }) + dispatch(setUser(user)) + const { data: urls } = await Ajax.get({ url: '/api/targets' }) - dispatch(setUrls(data)) + dispatch(setUrls(urls)) } catch (e) { dispatch({ type: ACTIONS.error, diff --git a/assets/js/containers/Carousel.js b/assets/js/containers/Carousel.js index ec6aaa4..a97fc9e 100644 --- a/assets/js/containers/Carousel.js +++ b/assets/js/containers/Carousel.js @@ -45,9 +45,11 @@ const CarouselItem = props => ( {props.button} - {props.footer && + {props.footers && props.footers.length && } ) diff --git a/assets/js/lib/Ajax.js b/assets/js/lib/Ajax.js index c05d09d..cef001f 100644 --- a/assets/js/lib/Ajax.js +++ b/assets/js/lib/Ajax.js @@ -86,28 +86,29 @@ export default class Ajax { } xhr.onerror = () => { var data = xhr.response + try { data = JSON.parse(data) } catch (e) {} // method not allowed if (xhr.status === 405) { - reject(new AjaxError('405 Method Not Allowed', data, xhr)) + reject(new AjaxError('405 Method Not Allowed', data.error || data, xhr)) return } else if (xhr.status === 404) { - reject(new AjaxError('404 Not Found', data, xhr)) + reject(new AjaxError('404 Not Found', data.error || data, xhr)) return } try { // if the access token is invalid, try to use the refresh token - var json = JSON.parse(data) + var json = data if (json.error === 'access_denied' && json.hint.includes('token') && json.hint.includes('invalid') && ajaxcfg.refresh_token) { return Ajax.refresh(opts) } else if (json.error === 'access_denied' && json.hint.includes('token') && json.hint.includes('revoked')) { reject(new AjaxError('token revoked', data, xhr)) } } catch (e) { - reject(new AjaxError(e.toString(), data, xhr)) + reject(new AjaxError(e.toString(), data.error || data, xhr)) } finally { - reject(new AjaxError(data, xhr.status, xhr)) + reject(new AjaxError(data.error || data, xhr.status, xhr)) } } diff --git a/assets/js/login.js b/assets/js/login.js index 6295d50..790ffdf 100644 --- a/assets/js/login.js +++ b/assets/js/login.js @@ -87,7 +87,8 @@ class App extends React.Component { onButtonClick={() => this.dispatch(signup(this.state.user.email, this.state.user.password))} smallButton='Have an account?' onSmallButtonClick={() => this.dispatch(setCarousel(1))} - footer='Sign up with your Google account' /> + footers={['Sign up with Google', 'Sign up with Github']} + footerHrefs={['/auth/google', '/auth/github']} /> this.dispatch(checkEmail(this.state.user.email))} smallButton='Create account' onSmallButtonClick={() => this.dispatch(setCarousel(0))} - footer='Sign in with your Google account' /> + footers={['Sign in with Google', 'Sign in with Github']} + footerHrefs={['/auth/google', '/auth/github']} /> { return { working: data } + case Actions.set_user: + return { + user: { + ...state.user, + ...data + } + } case Actions.list_url: return { urls: data || [] diff --git a/assets/js/targets.js b/assets/js/targets.js index 6ccd4f0..04fd330 100644 --- a/assets/js/targets.js +++ b/assets/js/targets.js @@ -5,7 +5,7 @@ import ReactDOM from 'react-dom' import Progress from './components/Progress' import UriListItem from './containers/UriListItem' import reducer from './reducers/targets' -import { fetchUrls, createNewUrl, setEditing } from './actions/targets' +import { fetchData, createNewUrl, setEditing } from './actions/targets' import '../styles/targets.scss' class App extends React.Component { @@ -38,7 +38,7 @@ class App extends React.Component { } } componentDidMount () { - this.dispatch(fetchUrls()) + this.dispatch(fetchData()) } getRegisteredUris () { return this.state.urls.map((item, i) => { @@ -55,6 +55,8 @@ class App extends React.Component {
diff --git a/assets/styles/lib/vars.scss b/assets/styles/lib/vars.scss index 6209ac7..a6d742d 100644 --- a/assets/styles/lib/vars.scss +++ b/assets/styles/lib/vars.scss @@ -24,8 +24,8 @@ $text-dark-2: $black-2; $text-light-1: white; $text-light-2: rgba(255,255,255,.75); -$accent-1: #731212; -$accent-2: #9a834d; -$accent-3: #D4DBF1; +$accent-1: #102237; +$accent-2: #18517c; +$accent-3: #4f91b8; $red: #FE4C52; diff --git a/assets/styles/shared/carousel.scss b/assets/styles/shared/carousel.scss index 0139b8f..5d83240 100644 --- a/assets/styles/shared/carousel.scss +++ b/assets/styles/shared/carousel.scss @@ -83,6 +83,7 @@ bottom: 20px; width: 100%; margin: 0; + line-height: 30px; color: $text-dark-2; .btn { @@ -91,6 +92,8 @@ a { color: $accent-2; text-decoration: none; + display: inline-block; + width: 100%; } } .button-row { diff --git a/config/env/development.js b/config/env/development.js index 7326a6a..7e5c205 100644 --- a/config/env/development.js +++ b/config/env/development.js @@ -347,7 +347,7 @@ module.exports = { * * ***************************************************************************/ custom: { - baseUrl: 'https://example.com', + baseURL: 'http://localhost:3000', internalEmailAddress: 'support@example.com' // mailgunDomain: 'mg.example.com', diff --git a/config/env/production.js b/config/env/production.js index 7d80a30..37892ef 100644 --- a/config/env/production.js +++ b/config/env/production.js @@ -348,7 +348,7 @@ module.exports = { * * ***************************************************************************/ custom: { - baseUrl: 'https://example.com', + baseURL: 'https://example.com', internalEmailAddress: 'support@example.com' // mailgunDomain: 'mg.example.com', diff --git a/config/passport.js b/config/passport.js index 7f54c0d..6e239dc 100644 --- a/config/passport.js +++ b/config/passport.js @@ -5,5 +5,17 @@ module.exports.passport = { local: { strategy: require('passport-local').Strategy + }, + google: { + strategy: require('passport-google-oauth20').Strategy, + protocol: 'oauth2', + callback: '/auth/google/callback', + options: {} + }, + github: { + strategy: require('passport-github2').Strategy, + protocol: 'oauth2', + callback: '/auth/github/callback', + options: {} } } diff --git a/config/protocols.js b/config/protocols.js index 6392ccc..0a6183e 100644 --- a/config/protocols.js +++ b/config/protocols.js @@ -90,6 +90,23 @@ module.exports.protocols = { return next(e) } } + }, + oauth2: { + login: async function (req, accessToken, refreshToken, profile, next) { + try { + const passportHelper = await sails.helpers.passport() + await passportHelper.connect(req, { + tokens: { + accessToken, + refreshToken + }, + identifier: profile.id, + protocol: 'oauth2' + }, profile, next) + } catch (e) { + return next(e, false) + } + } } }