Merge pull request #34 from EbookFoundation/feature/oauth2
add google and github oauth2 login supportpull/37/head
commit
6945fb0ff8
|
@ -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')
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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 || []
|
||||
|
|
|
@ -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' : '')}>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -347,7 +347,7 @@ module.exports = {
|
|||
* *
|
||||
***************************************************************************/
|
||||
custom: {
|
||||
baseUrl: 'https://example.com',
|
||||
baseURL: 'http://localhost:3000',
|
||||
internalEmailAddress: 'support@example.com'
|
||||
|
||||
// mailgunDomain: 'mg.example.com',
|
||||
|
|
|
@ -348,7 +348,7 @@ module.exports = {
|
|||
* *
|
||||
***************************************************************************/
|
||||
custom: {
|
||||
baseUrl: 'https://example.com',
|
||||
baseURL: 'https://example.com',
|
||||
internalEmailAddress: 'support@example.com'
|
||||
|
||||
// mailgunDomain: 'mg.example.com',
|
||||
|
|
|
@ -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: {}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue