Merge pull request #34 from EbookFoundation/feature/oauth2

add google and github oauth2 login support
pull/37/head
Theodore Kluge 2019-02-03 14:14:56 -05:00 committed by GitHub
commit 6945fb0ff8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 101 additions and 34 deletions

View File

@ -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')

View File

@ -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

View File

@ -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 {

View File

@ -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,

View File

@ -45,9 +45,11 @@ const CarouselItem = props => (
{props.button}
</button>
</div>
{props.footer &&
{props.footers && props.footers.length &&
<footer className='footer-row'>
<a href='#'>{props.footer}</a>
{
props.footers.map((x, i) => <a key={i} href={props.footerHrefs[i] || '#'}>{x}</a>)
}
</footer>}
</form>
)

View File

@ -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))
}
}

View File

@ -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']} />
<CarouselItem
header='Sign in'
@ -97,7 +98,8 @@ class App extends React.Component {
onButtonClick={() => 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']} />
<CarouselItem
header='Welcome'

View File

@ -10,6 +10,13 @@ const reducer = (state = {}, action) => {
return {
working: data
}
case Actions.set_user:
return {
user: {
...state.user,
...data
}
}
case Actions.list_url:
return {
urls: data || []

View File

@ -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 {
<aside className='nav nav-left'>
<header>
<h1>RoE</h1>
<span>{this.state.user.email}</span>
<a href='/logout'>Log out</a>
</header>
</aside>
<section className={'content flex' + (this.state.working ? ' working' : '')}>

View File

@ -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;

View File

@ -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 {

View File

@ -347,7 +347,7 @@ module.exports = {
* *
***************************************************************************/
custom: {
baseUrl: 'https://example.com',
baseURL: 'http://localhost:3000',
internalEmailAddress: 'support@example.com'
// mailgunDomain: 'mg.example.com',

View File

@ -348,7 +348,7 @@ module.exports = {
* *
***************************************************************************/
custom: {
baseUrl: 'https://example.com',
baseURL: 'https://example.com',
internalEmailAddress: 'support@example.com'
// mailgunDomain: 'mg.example.com',

View File

@ -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: {}
}
}

View File

@ -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)
}
}
}
}